From 59124c4caf513d6b6a5204e0b17d3e1965de9865 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 11 Oct 2023 12:56:00 -0700 Subject: [PATCH 01/97] Make a decorator for handling status changes --- pyiron_workflow/node.py | 31 ++++++++++++++++++ tests/unit/test_status_management.py | 47 ++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 tests/unit/test_status_management.py diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 374c01bed..7d3ad1605 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -23,6 +23,37 @@ from pyiron_workflow.io import Inputs, Outputs +def manage_status(node_method): + """ + Decorates methods of nodes that might be time-consuming, i.e. their main run + functionality. + + Sets `running` to true until the method completes and either fails or returns + something other than a `concurrent.futures.Future` instance; sets `failed` to true + if the method raises an exception; raises a `RuntimeError` if the node is already + `running` or `failed`. + """ + def wrapped_method(node: Node, *args, **kwargs): # rather node:Node + if node.running: + raise RuntimeError(f"{node.label} is already running") + elif node.failed: + raise RuntimeError(f"{node.label} has a failed status") + + node.running = True + try: + out = node_method(node, *args, **kwargs) + return out + except Exception as e: + node.failed = True + out = None + raise e + finally: + # Leave the status as running if the method returns a future + node.running = isinstance(out, Future) + + return wrapped_method + + class Node(HasToDict, ABC): """ Nodes are elements of a computational graph. diff --git a/tests/unit/test_status_management.py b/tests/unit/test_status_management.py new file mode 100644 index 000000000..e860cdfb3 --- /dev/null +++ b/tests/unit/test_status_management.py @@ -0,0 +1,47 @@ +from concurrent.futures import Future +from sys import version_info +from unittest import TestCase, skipUnless + +from pyiron_workflow.node import manage_status + + +class FauxNode: + def __init__(self): + self.running = False + self.failed = False + + @manage_status + def success(self, x): + return x / 2 + + @manage_status + def failure(self): + return 1 / 0 + + @manage_status + def future(self): + return Future() + + +@skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") +class TestStatusManagement(TestCase): + def setUp(self) -> None: + self.node = FauxNode() + + def test_success(self): + out = self.node.success(4) + self.assertFalse(self.node.running) + self.assertFalse(self.node.failed) + self.assertEqual(out, 2) + + def test_failure(self): + with self.assertRaises(ZeroDivisionError): + self.node.failure() + self.assertFalse(self.node.running) + self.assertTrue(self.node.failed) + + def test_future(self): + out = self.node.future() + self.assertTrue(self.node.running) + self.assertFalse(self.node.failed) + self.assertIsInstance(out, Future) From 83d491ddf582c0a837ca856d64511c3011024353 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 11 Oct 2023 13:13:23 -0700 Subject: [PATCH 02/97] Apply the new decorator to the run function Comes with a bit of a change in the API, the `run()` call no longer overrides being in a failed state -- you need to reset the failed state first. --- pyiron_workflow/node.py | 17 ++++------------- tests/unit/test_function.py | 14 +++++++++----- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 7d3ad1605..222d29ca1 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -85,7 +85,8 @@ class Node(HasToDict, ABC): The `run()` method returns a representation of the node output (possible a futures object, if the node is running on an executor), and consequently `update()` also - returns this output if the node is `ready`. + returns this output if the node is `ready`. Both `run()` and `update()` will raise + errors if the node is already running or has a failed status. Calling an already instantiated node allows its input channels to be updated using keyword arguments corresponding to the channel labels, performing a batch-update of @@ -214,25 +215,15 @@ def process_run_result(self, run_output: Any | tuple) -> None: """ pass + @manage_status def run(self) -> Any | tuple | Future: """ Executes the functionality of the node defined in `on_run`. Handles the status of the node, and communicating with any remote computing resources. """ - if self.running: - raise RuntimeError(f"{self.label} is already running") - - self.running = True - self.failed = False - if self.executor is None: - try: - run_output = self.on_run(**self.run_args) - except Exception as e: - self.running = False - self.failed = True - raise e + run_output = self.on_run(**self.run_args) return self.finish_run(run_output) else: # Just blindly try to execute -- as we nail down the executor interaction diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 1420d06da..8aaa9c4b9 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -227,7 +227,7 @@ def test_statuses(self): # The function error should get passed up n.run() self.assertFalse(n.ready) - # self.assertFalse(n.running) + self.assertFalse(n.running) self.assertTrue(n.failed) n.inputs.x = 1 @@ -236,14 +236,18 @@ def test_statuses(self): msg="Should not be ready while it has failed status" ) - n.run() + n.failed = False # Manually reset the failed status self.assertTrue( n.ready, - msg="A manual run() call bypasses checks, so readiness should reset" + msg="Input is ok, not running, not failed -- should be ready!" ) + n.run() self.assertTrue(n.ready) - # self.assertFalse(n.running) - self.assertFalse(n.failed, msg="Re-running should reset failed status") + self.assertFalse(n.running) + self.assertFalse( + n.failed, + msg="Should be back to a good state and ready to run again" + ) def test_with_self(self): def with_self(self, x: float) -> float: From 1958b6b00484fe71db1a4c967db66aad766cdd8c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 11 Oct 2023 13:20:03 -0700 Subject: [PATCH 03/97] Inline the length check on function output --- pyiron_workflow/function.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index 25c2c9a56..d35e88f12 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -496,12 +496,10 @@ def process_run_result(self, function_output): so that the node can finishing "running" and push its data forward when that execution is finished. """ - if len(self.outputs) == 0: - return - elif len(self.outputs) == 1: - function_output = (function_output,) - - for out, value in zip(self.outputs, function_output): + for out, value in zip( + self.outputs, + (function_output,) if len(self.outputs) == 1 else function_output + ): out.update(value) def _convert_input_args_and_kwargs_to_input_kwargs(self, *args, **kwargs): From 37de0365842d76aa783ff9c1342bf5dfef58d863 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 11 Oct 2023 13:26:26 -0700 Subject: [PATCH 04/97] Allow `process_run_result` to actually modify the return value --- pyiron_workflow/composite.py | 3 +++ pyiron_workflow/function.py | 11 +++-------- pyiron_workflow/node.py | 21 +++++++++++++-------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 59cb2a93d..86751dec7 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -165,6 +165,9 @@ def run_graph(self): node.run() return DotDict(self.outputs.to_value_dict()) + def process_run_result(self, run_output): + return run_output + def disconnect_run(self) -> list[tuple[Channel, Channel]]: """ Disconnect all `signals.input.run` connections on all child nodes. diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index d35e88f12..1b97ec9be 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -3,7 +3,7 @@ import inspect import warnings from functools import partialmethod -from typing import get_args, get_type_hints, Optional, TYPE_CHECKING +from typing import Any, get_args, get_type_hints, Optional, TYPE_CHECKING from pyiron_workflow.channels import InputData, OutputData, NotData from pyiron_workflow.has_channel import HasChannel @@ -486,21 +486,16 @@ def run_args(self) -> dict: kwargs["self"] = self return kwargs - def process_run_result(self, function_output): + def process_run_result(self, function_output: Any | tuple) -> Any | tuple: """ Take the results of the node function, and use them to update the node output. - - By extracting this as a separate method, we allow the node to pass the actual - execution off to another entity and release the python process to do other - things. In such a case, this function should be registered as a callback - so that the node can finishing "running" and push its data forward when that - execution is finished. """ for out, value in zip( self.outputs, (function_output,) if len(self.outputs) == 1 else function_output ): out.update(value) + return function_output def _convert_input_args_and_kwargs_to_input_kwargs(self, *args, **kwargs): reverse_keys = list(self._input_args.keys())[::-1] diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 222d29ca1..f61df0c1a 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -206,14 +206,19 @@ def run_args(self) -> dict: """ return {} - def process_run_result(self, run_output: Any | tuple) -> None: + @abstractmethod + def process_run_result(self, run_output): """ What to _do_ with the results of `on_run` once you have them. + By extracting this as a separate method, we allow the node to pass the actual + execution off to another entity and release the python process to do other + things. In such a case, this function should be registered as a callback + so that the node can process the result of that process. + Args: - run_output (tuple): The results of a `self.on_run(self.run_args)` call. + run_output: The results of a `self.on_run(self.run_args)` call. """ - pass @manage_status def run(self) -> Any | tuple | Future: @@ -239,18 +244,18 @@ def finish_run(self, run_output: tuple | Future) -> Any | tuple: By extracting this as a separate method, we allow the node to pass the actual execution off to another entity and release the python process to do other things. In such a case, this function should be registered as a callback - so that the node can finish "running" and, e.g. push its data forward when that - execution is finished. In such a case, a `concurrent.futures.Future` object is - expected back and must be unpacked. + so that the node can process the results, e.g. by unpacking the futures object, + formatting the results nicely, and/or updating its attributes (like output + channels). """ if isinstance(run_output, Future): run_output = run_output.result() self.running = False try: - self.process_run_result(run_output) + processed_output = self.process_run_result(run_output) self.signals.output.ran() - return run_output + return processed_output except Exception as e: self.failed = True raise e From ca0ca51b0958ca1ff89fbc4d12c5baf875281e18 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 11 Oct 2023 14:22:08 -0700 Subject: [PATCH 05/97] Have input channels pull instead of output channels pushing --- pyiron_workflow/channels.py | 75 +++++++++++++------------------------ pyiron_workflow/function.py | 2 +- pyiron_workflow/io.py | 2 +- pyiron_workflow/node.py | 7 +++- tests/unit/test_channels.py | 50 ++++++++++++------------- 5 files changed, 59 insertions(+), 77 deletions(-) diff --git a/pyiron_workflow/channels.py b/pyiron_workflow/channels.py index 5fe20999b..6acdfd5ac 100644 --- a/pyiron_workflow/channels.py +++ b/pyiron_workflow/channels.py @@ -2,20 +2,21 @@ Channels are access points for information to flow into and out of nodes. Data channels carry, unsurprisingly, data. -Output data channels will attempt to push their new value to all their connected input -data channels on update, while input data channels will reject any updates if their -parent node is running. -In this way, data channels facilitate forward propagation of data through a graph. -They hold data persistently. +Connections are only permissible between opposite sub-types, i.e. input-output. +When input channels `pull()` data in, they set their `value` to the first available +data value among their connections -- i.e. the `value` of the first output channel in +their connections who has something other than `NotData`. +Input data channels will raise an error if a `pull()` is attempted while their parent + node is running. Signal channels are tools for procedurally exposing functionality on nodes. Input signal channels are connected to a callback function which gets invoked when the -channel is updated. -Output signal channels must be accessed by the owning node directly, and then trigger -all the input signal channels to which they are connected. +channel is called. +Output signal channels call all the input channels they are connected to when they get + called themselves. In this way, signal channels can force behaviour (node method calls) to propagate forwards through a graph. -They do not hold any data, but rather fire for an effect. +They do not hold any data and have no `value` attribute, but rather fire for an effect. """ from __future__ import annotations @@ -32,7 +33,6 @@ ) if typing.TYPE_CHECKING: - from pyiron_workflow.composite import Composite from pyiron_workflow.node import Node @@ -239,29 +239,6 @@ def ready(self) -> bool: def _value_is_data(self): return self.value is not NotData - def update(self, value) -> None: - """ - Store a new value and trigger before- and after-update routines. - - Args: - value: The value to store. - """ - self._before_update() - self.value = value - self._after_update() - - def _before_update(self) -> None: - """ - A tool for child classes to do things before the value changed during an update. - """ - pass - - def _after_update(self) -> None: - """ - A tool for child classes to do things after the value changed during an update. - """ - pass - def connect(self, *others: DataChannel) -> None: """ For all others for which the connection is valid (one input, one output, both @@ -279,9 +256,6 @@ def connect(self, *others: DataChannel) -> None: if self._valid_connection(other): self.connections.append(other) other.connections.append(self) - out, inp = self._figure_out_who_is_who(other) - if out.value is not NotData: - inp.update(out.value) else: if isinstance(other, DataChannel): warn( @@ -331,8 +305,7 @@ def to_dict(self) -> dict: class InputData(DataChannel): """ - On `update`, Input channels will only `update` if their parent node is not - `running`. + `pull()` updates input data value to the first available data among connections. The `strict_connections` parameter controls whether connections are subject to type checking requirements. @@ -357,12 +330,25 @@ def __init__( ) self.strict_connections = strict_connections - def _before_update(self) -> None: + def pull(self) -> None: + """ + Sets `value` to the first value among connections that is something other than + `NotData`; if no such value exists (e.g. because there are no connections or + because all the connected output channels have `NotData` as their value), + `value` remains unchanged. + + Raises: + RuntimeError: If the parent node is `running`. + """ if self.node.running: raise RuntimeError( f"Parent node {self.node.label} of {self.label} is running, so value " f"cannot be updated." ) + for out in self.connections: + if out.value is not NotData: + self.value = out.value + break def activate_strict_connections(self) -> None: self.strict_connections = True @@ -372,16 +358,7 @@ def deactivate_strict_connections(self) -> None: class OutputData(DataChannel): - """ - On `update`, Output channels propagate their value (as long as it's actually data) - to all the input channels to which they are connected by invoking their `update` - method. - """ - - def _after_update(self) -> None: - if self._value_is_data: - for inp in self.connections: - inp.update(self.value) + pass class SignalChannel(Channel, ABC): diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index 1b97ec9be..209b59aac 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -494,7 +494,7 @@ def process_run_result(self, function_output: Any | tuple) -> Any | tuple: self.outputs, (function_output,) if len(self.outputs) == 1 else function_output ): - out.update(value) + out.value = value return function_output def _convert_input_args_and_kwargs_to_input_kwargs(self, *args, **kwargs): diff --git a/pyiron_workflow/io.py b/pyiron_workflow/io.py index ceebf57ef..4835a6e5b 100644 --- a/pyiron_workflow/io.py +++ b/pyiron_workflow/io.py @@ -168,7 +168,7 @@ class DataIO(IO, ABC): """ def _assign_a_non_channel_value(self, channel: DataChannel, value) -> None: - channel.update(value) + channel.value = value def to_value_dict(self): return {label: channel.value for label, channel in self.channel_dict.items()} diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index f61df0c1a..6243127eb 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -220,8 +220,13 @@ def process_run_result(self, run_output): run_output: The results of a `self.on_run(self.run_args)` call. """ + def run(self): + for inp in self.inputs: + inp.pull() + return self._run() + @manage_status - def run(self) -> Any | tuple | Future: + def _run(self) -> Any | tuple | Future: """ Executes the functionality of the node defined in `on_run`. Handles the status of the node, and communicating with any remote diff --git a/tests/unit/test_channels.py b/tests/unit/test_channels.py index 4dfd1d7f2..41567027c 100644 --- a/tests/unit/test_channels.py +++ b/tests/unit/test_channels.py @@ -23,6 +23,7 @@ def setUp(self) -> None: self.ni1 = InputData(label="numeric", node=DummyNode(), default=1, type_hint=int | float) self.ni2 = InputData(label="numeric", node=DummyNode(), default=1, type_hint=int | float) self.no = OutputData(label="numeric", node=DummyNode(), default=0, type_hint=int | float) + self.no_empty = OutputData(label="not_data", node=DummyNode(), type_hint=int | float) self.so1 = OutputData(label="list", node=DummyNode(), default=["foo"], type_hint=list) self.so2 = OutputData(label="list", node=DummyNode(), default=["foo"], type_hint=list) @@ -42,6 +43,8 @@ def test_connections(self): self.ni1.connect(self.no) self.assertIn(self.no, self.ni1.connections) self.assertIn(self.ni1, self.no.connections) + self.assertNotEqual(self.no.value, self.ni1.value) + self.ni1.pull() self.assertEqual(self.no.value, self.ni1.value) with self.subTest("Test disconnection"): @@ -73,29 +76,40 @@ def test_connections(self): with self.subTest("Test iteration"): self.assertTrue(all([con in self.no.connections for con in self.no])) - with self.subTest("Don't push NotData"): - self.no.disconnect_all() + with self.subTest("Data should update on pull"): + self.ni1.disconnect_all() + self.no.value = NotData self.ni1.value = 1 + + self.ni1.connect(self.no_empty) self.ni1.connect(self.no) self.assertEqual( self.ni1.value, 1, - msg="NotData should not be getting pushed on connection" + msg="Data should not be getting pushed on connection" + ) + self.ni1.pull() + self.assertEqual( + self.ni1.value, + 1, + msg="NotData values should not be getting pulled" ) - self.ni2.value = 2 self.no.value = 3 - self.ni2.connect(self.no) + self.ni1.pull() self.assertEqual( - self.ni2.value, + self.ni1.value, 3, - msg="Actual data should be getting pushed" + msg="Data pull should to first connected value that's actually data," + "in this case skipping over no_empty" ) - self.no.update(NotData) + self.no_empty.value = 4 + self.ni1.pull() self.assertEqual( - self.ni2.value, - 3, - msg="NotData should not be getting pushed on updates" + self.ni1.value, + 4, + msg="As soon as no_empty actually has data, it's position as 0th " + "element in the connections list should give it priority" ) def test_connection_validity_tests(self): @@ -155,20 +169,6 @@ def test_ready(self): self.ni1.value = "Not numeric at all" self.assertFalse(self.ni1.ready) - def test_update(self): - self.no.connect(self.ni1, self.ni2) - self.no.update(42) - for inp in self.no.connections: - self.assertEqual( - self.no.value, - inp.value, - msg="Value should have been passed downstream" - ) - - self.ni1.node.running = True - with self.assertRaises(RuntimeError): - self.no.update(42) - class TestSignalChannels(TestCase): def setUp(self) -> None: From d2975358ceb6fe43cd721dd214bbea5e0eb808ab Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 11 Oct 2023 15:10:12 -0700 Subject: [PATCH 06/97] Update the notebook --- notebooks/workflow_example.ipynb | 2207 ++++++++++++++++-------------- 1 file changed, 1185 insertions(+), 1022 deletions(-) diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index 8d21f40c6..727fc7de1 100644 --- a/notebooks/workflow_example.ipynb +++ b/notebooks/workflow_example.ipynb @@ -34,20 +34,7 @@ "execution_count": 1, "id": "8aca3b9b-9ba6-497a-ba9e-abdb15a6a5df", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "89ec887909114967be06c171de9e83c6", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from pyiron_workflow.function import Function" ] @@ -299,14 +286,6 @@ "adder_node.outputs.sum_.value" ] }, - { - "cell_type": "markdown", - "id": "263f5b24-113f-45d9-82cc-0475c59da587", - "metadata": {}, - "source": [ - "Note that assigning data to channels with `=` is actually just syntactic sugar for calling the `update` method of the underlying channel:" - ] - }, { "cell_type": "code", "execution_count": 12, @@ -325,7 +304,7 @@ } ], "source": [ - "adder_node.inputs.x.update(2)\n", + "adder_node.inputs.x = 2\n", "adder_node.update()" ] }, @@ -655,8 +634,8 @@ { "data": { "text/plain": [ - "array([0.45174171, 0.42157923, 0.505547 , 0.47028098, 0.43732173,\n", - " 0.50225988, 0.9376775 , 0.61550209, 0.81934053, 0.32220586])" + "array([0.23888392, 0.24728969, 0.2937081 , 0.66902714, 0.47716927,\n", + " 0.12469378, 0.36244931, 0.01119268, 0.26370445, 0.84121862])" ] }, "execution_count": 22, @@ -665,7 +644,7 @@ }, { "data": { - "image/png": "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", + "image/png": "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IoyMjIxIkrKysgxXAgAA7BoZGZHb7Z7x7w7rQnHlEjAxMaHTp08rJSVFDof9B5kNDw8rKytLvb29Sk1NnYcKMdcYs9jDmMUWxiv2xOKYWZalkZERXXHFFUpImPnKkJiYGUlISNDy5csv+n1SU1NjZgBxBmMWexiz2MJ4xZ5YG7PzzYhM4gJWAABgFGEEAAAYFRdhJDk5WTU1NUpOTjZdCmaJMYs9jFlsYbxiz0Ies5i4gBUAACxccTEzAgAALl2EEQAAYBRhBAAAGEUYAQAARi2YMFJfX6+cnBy5XC55vV61tLSct/8777wjr9crl8ulq6++Wt/+9rejVCkm2RmzN998U+vXr9dnPvMZpaamqqSkRD/60Y+iWC3s/hub9O677yoxMVE33HDD/BaIMHbHbGxsTLt371Z2draSk5O1cuVK7d+/P0rVQrI/ZocOHdL111+vyy67TB6PR/fcc48GBwejVO0cshaA7373u9aiRYusF154werq6rIefPBBa8mSJdb//d//Tdv/xIkT1mWXXWY9+OCDVldXl/XCCy9YixYtsl5//fUoVx6/7I7Zgw8+aD355JNWW1ub9f7771vV1dXWokWLrGPHjkW58vhkd7wm/epXv7Kuvvpqq6yszLr++uujUywsy4pszL7whS9YxcXFls/ns7q7u63/+q//st59990oVh3f7I5ZS0uLlZCQYD377LPWiRMnrJaWFmvNmjXWpk2bolz5xVsQYaSoqMiqrKwMabv22mutRx99dNr+f/u3f2tde+21IW333nuvdeONN85bjQhld8ymk5eXZ9XW1s51aZhGpONVUVFh/f3f/71VU1NDGIkyu2P2wx/+0HK73dbg4GA0ysM07I7Z17/+devqq68OafvmN79pLV++fN5qnC8xv0wzPj6u9vZ2lZWVhbSXlZWptbV12nN+8pOfhPX//Oc/r6NHj+qTTz6Zt1pxRiRjdq6JiQmNjIwoLS1tPkrEWSIdr5deekkffvihampq5rtEnCOSMfvBD36gwsJCfe1rX9OVV16pa665Rg8//LB++9vfRqPkuBfJmJWWlurkyZNqamqSZVn66KOP9Prrr+uOO+6IRslzKiYelHc+AwMDCgaDysjICGnPyMhQX1/ftOf09fVN2//TTz/VwMCAPB7PvNWLyMbsXE899ZR+85vf6M4775yPEnGWSMbrgw8+0KOPPqqWlhYlJsb8f2ZiTiRjduLECR05ckQul0vf+973NDAwoK985SsaGhriupEoiGTMSktLdejQIVVUVGh0dFSffvqpvvCFL+hb3/pWNEqeUzE/MzLJ4XCEvLYsK6ztQv2na8f8sTtmk1599VU9/vjjamxs1LJly+arPJxjtuMVDAZ11113qba2Vtdcc020ysM07Pwbm5iYkMPh0KFDh1RUVKTbb79dTz/9tF5++WVmR6LIzph1dXXpgQce0GOPPab29nY1Nzeru7tblZWV0Sh1TsX8/7Kkp6fL6XSGJcf+/v6whDkpMzNz2v6JiYlaunTpvNWKMyIZs0mNjY3avn27XnvtNd12223zWSZ+x+54jYyM6OjRo+ro6ND9998v6cwPnWVZSkxM1FtvvaVbbrklKrXHq0j+jXk8Hl155ZUhj3vPzc2VZVk6efKkVq1aNa81x7tIxqyurk5r167VI488Ikm67rrrtGTJEq1bt05PPPFETM3yx/zMSFJSkrxer3w+X0i7z+dTaWnptOeUlJSE9X/rrbdUWFioRYsWzVutOCOSMZPOzIhs27ZNr7zySkyuicYqu+OVmpqqn/70pzp+/PjUUVlZqdWrV+v48eMqLi6OVulxK5J/Y2vXrtXp06f161//eqrt/fffV0JCgpYvXz6v9SKyMfv444+VkBD6M+50OiX9/9n+mGHqytm5NHk71L59+6yuri6rqqrKWrJkifWLX/zCsizLevTRR60tW7ZM9Z+8tfehhx6yurq6rH379nFrb5TZHbNXXnnFSkxMtJ577jnL7/dPHb/61a9MfYW4Yne8zsXdNNFnd8xGRkas5cuXW3/6p39q/exnP7Peeecda9WqVdaOHTtMfYW4Y3fMXnrpJSsxMdGqr6+3PvzwQ+vIkSNWYWGhVVRUZOorRGxBhBHLsqznnnvOys7OtpKSkqyCggLrnXfemfrb3Xffbd10000h/d9++23rs5/9rJWUlGStWLHCamhoiHLFsDNmN910kyUp7Lj77rujX3icsvtv7GyEETPsjtl7771n3XbbbdbixYut5cuXW7t27bI+/vjjKFcd3+yO2Te/+U0rLy/PWrx4seXxeKy/+Iu/sE6ePBnlqi+ew7JibS4HAAAsJDF/zQgAAIhthBEAAGAUYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABG/T8nSJ5zOJcZ9gAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -915,306 +894,312 @@ "\n", "\n", - "\n", + "\n", "\n", "clustersimple\n", - "\n", - "simple: Workflow\n", + "\n", + "simple: Workflow\n", "\n", "clustersimpleInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", "\n", "clustersimpleOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", "\n", "clustersimplea\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "a: AddOne\n", + "\n", + "a: AddOne\n", "\n", "\n", "clustersimpleaInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", "\n", "clustersimpleaOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", "\n", "clustersimpleb\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "b: AddOne\n", + "\n", + "b: AddOne\n", "\n", "\n", "clustersimplebInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", "\n", "clustersimplebOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", "\n", "clustersimplesum\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "sum: AddNode\n", + "\n", + "sum: AddNode\n", "\n", "\n", "clustersimplesumInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", "\n", "clustersimplesumOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", "\n", "\n", "clustersimpleInputsrun\n", - "\n", - "run\n", + "\n", + "run\n", "\n", "\n", - "\n", + "\n", "clustersimpleOutputsran\n", - "\n", - "ran\n", + "\n", + "ran\n", "\n", "\n", - "\n", + "\n", "\n", - "clustersimpleInputsx\n", - "\n", - "x\n", + "clustersimpleInputsax\n", + "\n", + "ax\n", "\n", "\n", - "\n", + "\n", "clustersimpleaInputsx\n", - "\n", - "x\n", + "\n", + "x\n", "\n", - "\n", + "\n", "\n", - "clustersimpleInputsx->clustersimpleaInputsx\n", - "\n", - "\n", - "\n", + "clustersimpleInputsax->clustersimpleaInputsx\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clustersimpleInputsb__x\n", + "\n", + "b__x\n", "\n", "\n", - "\n", + "\n", "clustersimplebInputsx\n", - "\n", - "x\n", + "\n", + "x\n", "\n", - "\n", + "\n", "\n", - "clustersimpleInputsx->clustersimplebInputsx\n", - "\n", - "\n", - "\n", + "clustersimpleInputsb__x->clustersimplebInputsx\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clustersimpleOutputsy\n", - "\n", - "y\n", - "\n", - "\n", + "\n", "\n", - "clustersimpleOutputssum\n", - "\n", - "sum\n", + "clustersimpleOutputsay\n", + "\n", + "ay\n", "\n", - "\n", + "\n", "\n", + "clustersimpleOutputsa + b + 2\n", + "\n", + "a + b + 2\n", + "\n", + "\n", + "\n", "clustersimpleaInputsrun\n", - "\n", - "run\n", + "\n", + "run\n", "\n", "\n", - "\n", + "\n", "clustersimpleaOutputsran\n", - "\n", - "ran\n", + "\n", + "ran\n", "\n", "\n", "\n", - "\n", + "\n", "clustersimplebInputsrun\n", - "\n", - "run\n", + "\n", + "run\n", "\n", "\n", "\n", "clustersimpleaOutputsran->clustersimplebInputsrun\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "clustersimpleaOutputsy\n", - "\n", - "y\n", + "\n", + "y\n", "\n", - "\n", + "\n", "\n", - "clustersimpleaOutputsy->clustersimpleOutputsy\n", - "\n", - "\n", - "\n", + "clustersimpleaOutputsy->clustersimpleOutputsay\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "clustersimplesumInputsx\n", - "\n", - "x\n", + "\n", + "x\n", "\n", "\n", "\n", "clustersimpleaOutputsy->clustersimplesumInputsx\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "clustersimplebOutputsran\n", - "\n", - "ran\n", + "\n", + "ran\n", "\n", "\n", "\n", - "\n", + "\n", "clustersimplesumInputsrun\n", - "\n", - "run\n", + "\n", + "run\n", "\n", "\n", "\n", "clustersimplebOutputsran->clustersimplesumInputsrun\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "clustersimplebOutputsy\n", - "\n", - "y\n", + "\n", + "y\n", "\n", "\n", - "\n", + "\n", "clustersimplesumInputsy\n", - "\n", - "y\n", + "\n", + "y\n", "\n", "\n", "\n", "clustersimplebOutputsy->clustersimplesumInputsy\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "clustersimplesumOutputsran\n", - "\n", - "ran\n", + "\n", + "ran\n", "\n", "\n", "\n", - "\n", + "\n", "clustersimplesumOutputssum\n", - "\n", - "sum\n", + "\n", + "sum\n", "\n", - "\n", + "\n", "\n", - "clustersimplesumOutputssum->clustersimpleOutputssum\n", - "\n", - "\n", - "\n", + "clustersimplesumOutputssum->clustersimpleOutputsa + b + 2\n", + "\n", + "\n", + "\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 28, @@ -1242,6 +1227,26 @@ "id": "ae500d5e-e55b-432c-8b5f-d5892193cdf5", "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9dbe327786644b2ca0ea31216fc48bc9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n" + ] + }, { "name": "stdout", "output_type": "stream", @@ -1252,7 +1257,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 29, @@ -1261,7 +1266,7 @@ }, { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -1309,202 +1314,202 @@ "\n", "\n", - "\n", + "\n", "\n", "clusterwith_prebuilt\n", - "\n", - "with_prebuilt: Workflow\n", + "\n", + "with_prebuilt: Workflow\n", "\n", "clusterwith_prebuiltInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", "\n", "clusterwith_prebuiltOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", "\n", "\n", "clusterwith_prebuiltInputsrun\n", - "\n", - "run\n", + "\n", + "run\n", "\n", "\n", "\n", "clusterwith_prebuiltOutputsran\n", - "\n", - "ran\n", + "\n", + "ran\n", "\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltInputsname\n", - "\n", - "name\n", + "clusterwith_prebuiltInputsstructure__name\n", + "\n", + "structure__name\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltInputscrystalstructure\n", - "\n", - "crystalstructure\n", + "clusterwith_prebuiltInputsstructure__crystalstructure\n", + "\n", + "structure__crystalstructure\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltInputsa\n", - "\n", - "a\n", + "clusterwith_prebuiltInputsstructure__a\n", + "\n", + "structure__a\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltInputsc\n", - "\n", - "c\n", + "clusterwith_prebuiltInputsstructure__c\n", + "\n", + "structure__c\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltInputscovera\n", - "\n", - "covera\n", + "clusterwith_prebuiltInputsstructure__covera\n", + "\n", + "structure__covera\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltInputsu\n", - "\n", - "u\n", + "clusterwith_prebuiltInputsstructure__u\n", + "\n", + "structure__u\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltInputsorthorhombic\n", - "\n", - "orthorhombic\n", + "clusterwith_prebuiltInputsstructure__orthorhombic\n", + "\n", + "structure__orthorhombic\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltInputscubic\n", - "\n", - "cubic\n", + "clusterwith_prebuiltInputsstructure__cubic\n", + "\n", + "structure__cubic\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltInputsn_ionic_steps\n", - "\n", - "n_ionic_steps: int\n", + "clusterwith_prebuiltInputscalc__n_ionic_steps\n", + "\n", + "calc__n_ionic_steps: int\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltInputsn_print\n", - "\n", - "n_print: int\n", + "clusterwith_prebuiltInputscalc__n_print\n", + "\n", + "calc__n_print: int\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltInputstemperature\n", - "\n", - "temperature\n", + "clusterwith_prebuiltInputscalc__temperature\n", + "\n", + "calc__temperature\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltInputspressure\n", - "\n", - "pressure\n", + "clusterwith_prebuiltInputscalc__pressure\n", + "\n", + "calc__pressure\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltOutputscells\n", - "\n", - "cells\n", + "clusterwith_prebuiltOutputscalc__cells\n", + "\n", + "calc__cells\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltOutputsdisplacements\n", - "\n", - "displacements\n", + "clusterwith_prebuiltOutputscalc__displacements\n", + "\n", + "calc__displacements\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltOutputsenergy_pot\n", - "\n", - "energy_pot\n", + "clusterwith_prebuiltOutputscalc__energy_pot\n", + "\n", + "calc__energy_pot\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltOutputsenergy_tot\n", - "\n", - "energy_tot\n", + "clusterwith_prebuiltOutputscalc__energy_tot\n", + "\n", + "calc__energy_tot\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltOutputsforce_max\n", - "\n", - "force_max\n", + "clusterwith_prebuiltOutputscalc__force_max\n", + "\n", + "calc__force_max\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltOutputsforces\n", - "\n", - "forces\n", + "clusterwith_prebuiltOutputscalc__forces\n", + "\n", + "calc__forces\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltOutputsindices\n", - "\n", - "indices\n", + "clusterwith_prebuiltOutputscalc__indices\n", + "\n", + "calc__indices\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltOutputspositions\n", - "\n", - "positions\n", + "clusterwith_prebuiltOutputscalc__positions\n", + "\n", + "calc__positions\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltOutputspressures\n", - "\n", - "pressures\n", + "clusterwith_prebuiltOutputscalc__pressures\n", + "\n", + "calc__pressures\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltOutputstotal_displacements\n", - "\n", - "total_displacements\n", + "clusterwith_prebuiltOutputscalc__total_displacements\n", + "\n", + "calc__total_displacements\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltOutputsunwrapped_positions\n", - "\n", - "unwrapped_positions\n", + "clusterwith_prebuiltOutputscalc__unwrapped_positions\n", + "\n", + "calc__unwrapped_positions\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltOutputsvolume\n", - "\n", - "volume\n", + "clusterwith_prebuiltOutputscalc__volume\n", + "\n", + "calc__volume\n", "\n", - "\n", + "\n", "\n", - "clusterwith_prebuiltOutputsfig\n", - "\n", - "fig\n", + "clusterwith_prebuiltOutputsplot__fig\n", + "\n", + "plot__fig\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 30, @@ -1542,6 +1547,14 @@ "id": "2b9bb21a-73cd-444e-84a9-100e202aa422", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n" + ] + }, { "data": { "text/plain": [ @@ -1704,1086 +1717,1218 @@ "\n", "\n", - "\n", + "\n", "\n", "clusterphase_preference\n", - "\n", - "phase_preference: Workflow\n", + "\n", + "phase_preference: Workflow\n", "\n", "clusterphase_preferenceInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", "\n", "clusterphase_preferenceOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", "\n", "clusterphase_preferenceelement\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "element: UserInput\n", + "\n", + "element: UserInput\n", "\n", "\n", "clusterphase_preferenceelementInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", "\n", "clusterphase_preferenceelementOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", "\n", "clusterphase_preferencemin_phase1\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "min_phase1: LammpsMinimize\n", + "\n", + "min_phase1: LammpsMinimize\n", "\n", "\n", "clusterphase_preferencemin_phase1Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", "\n", "clusterphase_preferencemin_phase1Outputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", "\n", "clusterphase_preferencemin_phase2\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "min_phase2: LammpsMinimize\n", + "\n", + "min_phase2: LammpsMinimize\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", "\n", "clusterphase_preferencecompare\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "compare: PerAtomEnergyDifference\n", + "\n", + "compare: PerAtomEnergyDifference\n", "\n", "\n", "clusterphase_preferencecompareInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", "\n", "clusterphase_preferencecompareOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", "\n", "\n", "clusterphase_preferenceInputsrun\n", - "\n", - "run\n", + "\n", + "run\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferenceOutputsran\n", - "\n", - "ran\n", + "\n", + "ran\n", "\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputsuser_input\n", - "\n", - "user_input\n", + "clusterphase_preferenceInputselement\n", + "\n", + "element\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferenceelementInputsuser_input\n", - "\n", - "user_input\n", + "\n", + "user_input\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputsuser_input->clusterphase_preferenceelementInputsuser_input\n", - "\n", - "\n", - "\n", + "clusterphase_preferenceInputselement->clusterphase_preferenceelementInputsuser_input\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputscrystalstructure\n", - "\n", - "crystalstructure\n", + "clusterphase_preferenceInputsphase1\n", + "\n", + "phase1\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferencemin_phase1Inputscrystalstructure\n", - "\n", - "crystalstructure\n", + "\n", + "crystalstructure\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputscrystalstructure->clusterphase_preferencemin_phase1Inputscrystalstructure\n", - "\n", - "\n", - "\n", + "clusterphase_preferenceInputsphase1->clusterphase_preferencemin_phase1Inputscrystalstructure\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputscrystalstructure\n", - "\n", - "crystalstructure\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputscrystalstructure->clusterphase_preferencemin_phase2Inputscrystalstructure\n", - "\n", - "\n", - "\n", - "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputsa\n", - "\n", - "a\n", + "clusterphase_preferenceInputslattice_guess1\n", + "\n", + "lattice_guess1\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputsa\n", - "\n", - "a\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputslattice_guess\n", + "\n", + "lattice_guess\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputsa->clusterphase_preferencemin_phase1Inputsa\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputsa\n", - "\n", - "a\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsa->clusterphase_preferencemin_phase2Inputsa\n", - "\n", - "\n", - "\n", + "clusterphase_preferenceInputslattice_guess1->clusterphase_preferencemin_phase1Inputslattice_guess\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputsc\n", - "\n", - "c\n", + "clusterphase_preferenceInputsmin_phase1__structure__c\n", + "\n", + "min_phase1__structure__c\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputsc\n", - "\n", - "c\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputsstructure__c\n", + "\n", + "structure__c\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputsc->clusterphase_preferencemin_phase1Inputsc\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputsc\n", - "\n", - "c\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsc->clusterphase_preferencemin_phase2Inputsc\n", - "\n", - "\n", - "\n", + "clusterphase_preferenceInputsmin_phase1__structure__c->clusterphase_preferencemin_phase1Inputsstructure__c\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputscovera\n", - "\n", - "covera\n", + "clusterphase_preferenceInputsmin_phase1__structure__covera\n", + "\n", + "min_phase1__structure__covera\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputscovera\n", - "\n", - "covera\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputsstructure__covera\n", + "\n", + "structure__covera\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputscovera->clusterphase_preferencemin_phase1Inputscovera\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputscovera\n", - "\n", - "covera\n", + "clusterphase_preferenceInputsmin_phase1__structure__covera->clusterphase_preferencemin_phase1Inputsstructure__covera\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferenceInputscovera->clusterphase_preferencemin_phase2Inputscovera\n", - "\n", - "\n", - "\n", - "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputsu\n", - "\n", - "u\n", + "clusterphase_preferenceInputsmin_phase1__structure__u\n", + "\n", + "min_phase1__structure__u\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputsu\n", - "\n", - "u\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputsstructure__u\n", + "\n", + "structure__u\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputsu->clusterphase_preferencemin_phase1Inputsu\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputsu\n", - "\n", - "u\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsu->clusterphase_preferencemin_phase2Inputsu\n", - "\n", - "\n", - "\n", + "clusterphase_preferenceInputsmin_phase1__structure__u->clusterphase_preferencemin_phase1Inputsstructure__u\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputsorthorhombic\n", - "\n", - "orthorhombic\n", + "clusterphase_preferenceInputsmin_phase1__structure__orthorhombic\n", + "\n", + "min_phase1__structure__orthorhombic\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputsorthorhombic\n", - "\n", - "orthorhombic\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputsstructure__orthorhombic\n", + "\n", + "structure__orthorhombic\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputsorthorhombic->clusterphase_preferencemin_phase1Inputsorthorhombic\n", - "\n", - "\n", - "\n", + "clusterphase_preferenceInputsmin_phase1__structure__orthorhombic->clusterphase_preferencemin_phase1Inputsstructure__orthorhombic\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputsorthorhombic\n", - "\n", - "orthorhombic\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsorthorhombic->clusterphase_preferencemin_phase2Inputsorthorhombic\n", - "\n", - "\n", - "\n", - "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputscubic\n", - "\n", - "cubic\n", + "clusterphase_preferenceInputsmin_phase1__structure__cubic\n", + "\n", + "min_phase1__structure__cubic\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputscubic\n", - "\n", - "cubic\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputsstructure__cubic\n", + "\n", + "structure__cubic\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputscubic->clusterphase_preferencemin_phase1Inputscubic\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputscubic\n", - "\n", - "cubic\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputscubic->clusterphase_preferencemin_phase2Inputscubic\n", - "\n", - "\n", - "\n", + "clusterphase_preferenceInputsmin_phase1__structure__cubic->clusterphase_preferencemin_phase1Inputsstructure__cubic\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputsn_ionic_steps\n", - "\n", - "n_ionic_steps: int\n", + "clusterphase_preferenceInputsmin_phase1__calc__n_ionic_steps\n", + "\n", + "min_phase1__calc__n_ionic_steps: int\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputsn_ionic_steps\n", - "\n", - "n_ionic_steps: int\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputscalc__n_ionic_steps\n", + "\n", + "calc__n_ionic_steps: int\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputsn_ionic_steps->clusterphase_preferencemin_phase1Inputsn_ionic_steps\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputsn_ionic_steps\n", - "\n", - "n_ionic_steps: int\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsn_ionic_steps->clusterphase_preferencemin_phase2Inputsn_ionic_steps\n", - "\n", - "\n", - "\n", + "clusterphase_preferenceInputsmin_phase1__calc__n_ionic_steps->clusterphase_preferencemin_phase1Inputscalc__n_ionic_steps\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputsn_print\n", - "\n", - "n_print: int\n", + "clusterphase_preferenceInputsmin_phase1__calc__n_print\n", + "\n", + "min_phase1__calc__n_print: int\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputsn_print\n", - "\n", - "n_print: int\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputscalc__n_print\n", + "\n", + "calc__n_print: int\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputsn_print->clusterphase_preferencemin_phase1Inputsn_print\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputsn_print\n", - "\n", - "n_print: int\n", + "clusterphase_preferenceInputsmin_phase1__calc__n_print->clusterphase_preferencemin_phase1Inputscalc__n_print\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferenceInputsn_print->clusterphase_preferencemin_phase2Inputsn_print\n", - "\n", - "\n", - "\n", - "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputspressure\n", - "\n", - "pressure\n", + "clusterphase_preferenceInputsmin_phase1__calc__pressure\n", + "\n", + "min_phase1__calc__pressure\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputspressure\n", - "\n", - "pressure\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputscalc__pressure\n", + "\n", + "calc__pressure\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceInputspressure->clusterphase_preferencemin_phase1Inputspressure\n", - "\n", - "\n", - "\n", + "clusterphase_preferenceInputsmin_phase1__calc__pressure->clusterphase_preferencemin_phase1Inputscalc__pressure\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputspressure\n", - "\n", - "pressure\n", + "\n", + "\n", + "clusterphase_preferenceInputsphase2\n", + "\n", + "phase2\n", "\n", - "\n", - "\n", - "clusterphase_preferenceInputspressure->clusterphase_preferencemin_phase2Inputspressure\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputscrystalstructure\n", + "\n", + "crystalstructure\n", "\n", - "\n", + "\n", + "\n", + "clusterphase_preferenceInputsphase2->clusterphase_preferencemin_phase2Inputscrystalstructure\n", + "\n", + "\n", + "\n", + "\n", + "\n", "\n", - "clusterphase_preferenceOutputscells\n", - "\n", - "cells\n", + "clusterphase_preferenceInputslattice_guess2\n", + "\n", + "lattice_guess2\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputslattice_guess\n", + "\n", + "lattice_guess\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputslattice_guess2->clusterphase_preferencemin_phase2Inputslattice_guess\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceOutputsdisplacements\n", - "\n", - "displacements\n", + "clusterphase_preferenceInputsmin_phase2__structure__c\n", + "\n", + "min_phase2__structure__c\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputsstructure__c\n", + "\n", + "structure__c\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__c->clusterphase_preferencemin_phase2Inputsstructure__c\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceOutputsenergy_tot\n", - "\n", - "energy_tot\n", + "clusterphase_preferenceInputsmin_phase2__structure__covera\n", + "\n", + "min_phase2__structure__covera\n", "\n", - "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputsstructure__covera\n", + "\n", + "structure__covera\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__covera->clusterphase_preferencemin_phase2Inputsstructure__covera\n", + "\n", + "\n", + "\n", + "\n", + "\n", "\n", - "clusterphase_preferenceOutputsforce_max\n", - "\n", - "force_max\n", + "clusterphase_preferenceInputsmin_phase2__structure__u\n", + "\n", + "min_phase2__structure__u\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputsstructure__u\n", + "\n", + "structure__u\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__u->clusterphase_preferencemin_phase2Inputsstructure__u\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceOutputsforces\n", - "\n", - "forces\n", + "clusterphase_preferenceInputsmin_phase2__structure__orthorhombic\n", + "\n", + "min_phase2__structure__orthorhombic\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputsstructure__orthorhombic\n", + "\n", + "structure__orthorhombic\n", "\n", - "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__orthorhombic->clusterphase_preferencemin_phase2Inputsstructure__orthorhombic\n", + "\n", + "\n", + "\n", + "\n", + "\n", "\n", - "clusterphase_preferenceOutputsindices\n", - "\n", - "indices\n", + "clusterphase_preferenceInputsmin_phase2__structure__cubic\n", + "\n", + "min_phase2__structure__cubic\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputsstructure__cubic\n", + "\n", + "structure__cubic\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__cubic->clusterphase_preferencemin_phase2Inputsstructure__cubic\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceOutputspositions\n", - "\n", - "positions\n", + "clusterphase_preferenceInputsmin_phase2__calc__n_ionic_steps\n", + "\n", + "min_phase2__calc__n_ionic_steps: int\n", "\n", - "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputscalc__n_ionic_steps\n", + "\n", + "calc__n_ionic_steps: int\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__calc__n_ionic_steps->clusterphase_preferencemin_phase2Inputscalc__n_ionic_steps\n", + "\n", + "\n", + "\n", + "\n", + "\n", "\n", - "clusterphase_preferenceOutputspressures\n", - "\n", - "pressures\n", + "clusterphase_preferenceInputsmin_phase2__calc__n_print\n", + "\n", + "min_phase2__calc__n_print: int\n", "\n", - "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputscalc__n_print\n", + "\n", + "calc__n_print: int\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__calc__n_print->clusterphase_preferencemin_phase2Inputscalc__n_print\n", + "\n", + "\n", + "\n", + "\n", + "\n", "\n", - "clusterphase_preferenceOutputssteps\n", - "\n", - "steps\n", + "clusterphase_preferenceInputsmin_phase2__calc__pressure\n", + "\n", + "min_phase2__calc__pressure\n", "\n", - "\n", - "\n", - "clusterphase_preferenceOutputstotal_displacements\n", - "\n", - "total_displacements\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputscalc__pressure\n", + "\n", + "calc__pressure\n", "\n", - "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__calc__pressure->clusterphase_preferencemin_phase2Inputscalc__pressure\n", + "\n", + "\n", + "\n", + "\n", + "\n", "\n", - "clusterphase_preferenceOutputsunwrapped_positions\n", - "\n", - "unwrapped_positions\n", + "clusterphase_preferenceOutputsmin_phase1__calc__cells\n", + "\n", + "min_phase1__calc__cells\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceOutputsvolume\n", - "\n", - "volume\n", + "clusterphase_preferenceOutputsmin_phase1__calc__displacements\n", + "\n", + "min_phase1__calc__displacements\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceOutputsde\n", - "\n", - "de\n", + "clusterphase_preferenceOutputsmin_phase1__calc__energy_tot\n", + "\n", + "min_phase1__calc__energy_tot\n", "\n", - "\n", + "\n", "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__force_max\n", + "\n", + "min_phase1__calc__force_max\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__forces\n", + "\n", + "min_phase1__calc__forces\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__indices\n", + "\n", + "min_phase1__calc__indices\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__positions\n", + "\n", + "min_phase1__calc__positions\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__pressures\n", + "\n", + "min_phase1__calc__pressures\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__steps\n", + "\n", + "min_phase1__calc__steps\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__total_displacements\n", + "\n", + "min_phase1__calc__total_displacements\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__unwrapped_positions\n", + "\n", + "min_phase1__calc__unwrapped_positions\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__volume\n", + "\n", + "min_phase1__calc__volume\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__cells\n", + "\n", + "min_phase2__calc__cells\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__displacements\n", + "\n", + "min_phase2__calc__displacements\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__energy_tot\n", + "\n", + "min_phase2__calc__energy_tot\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__force_max\n", + "\n", + "min_phase2__calc__force_max\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__forces\n", + "\n", + "min_phase2__calc__forces\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__indices\n", + "\n", + "min_phase2__calc__indices\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__positions\n", + "\n", + "min_phase2__calc__positions\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__pressures\n", + "\n", + "min_phase2__calc__pressures\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__steps\n", + "\n", + "min_phase2__calc__steps\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__total_displacements\n", + "\n", + "min_phase2__calc__total_displacements\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__unwrapped_positions\n", + "\n", + "min_phase2__calc__unwrapped_positions\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__volume\n", + "\n", + "min_phase2__calc__volume\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputscompare__de\n", + "\n", + "compare__de\n", + "\n", + "\n", + "\n", "clusterphase_preferenceelementInputsrun\n", - "\n", - "run\n", + "\n", + "run\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferenceelementOutputsran\n", - "\n", - "ran\n", + "\n", + "ran\n", "\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferenceelementOutputsuser_input\n", - "\n", - "user_input\n", + "\n", + "user_input\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputsname\n", - "\n", - "name\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputselement\n", + "\n", + "element\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceelementOutputsuser_input->clusterphase_preferencemin_phase1Inputsname\n", - "\n", - "\n", - "\n", + "clusterphase_preferenceelementOutputsuser_input->clusterphase_preferencemin_phase1Inputselement\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputsname\n", - "\n", - "name\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputselement\n", + "\n", + "element\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferenceelementOutputsuser_input->clusterphase_preferencemin_phase2Inputsname\n", - "\n", - "\n", - "\n", + "clusterphase_preferenceelementOutputsuser_input->clusterphase_preferencemin_phase2Inputselement\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferencemin_phase1Inputsrun\n", - "\n", - "run\n", + "\n", + "run\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferencemin_phase1Outputsran\n", - "\n", - "ran\n", + "\n", + "ran\n", "\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferencemin_phase1Outputsstructure\n", - "\n", - "structure\n", + "\n", + "structure\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferencecompareInputsstructure1\n", - "\n", - "structure1\n", + "\n", + "structure1\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase1Outputsstructure->clusterphase_preferencecompareInputsstructure1\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscells\n", - "\n", - "cells\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__cells\n", + "\n", + "calc__cells\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase1Outputscells->clusterphase_preferenceOutputscells\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase1Outputscalc__cells->clusterphase_preferenceOutputsmin_phase1__calc__cells\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputsdisplacements\n", - "\n", - "displacements\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__displacements\n", + "\n", + "calc__displacements\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase1Outputsdisplacements->clusterphase_preferenceOutputsdisplacements\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase1Outputscalc__displacements->clusterphase_preferenceOutputsmin_phase1__calc__displacements\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputsenergy_pot\n", - "\n", - "energy_pot\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputsenergy\n", + "\n", + "energy\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferencecompareInputsenergy1\n", - "\n", - "energy1\n", + "\n", + "energy1\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase1Outputsenergy_pot->clusterphase_preferencecompareInputsenergy1\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase1Outputsenergy->clusterphase_preferencecompareInputsenergy1\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputsenergy_tot\n", - "\n", - "energy_tot\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__energy_tot\n", + "\n", + "calc__energy_tot\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase1Outputsenergy_tot->clusterphase_preferenceOutputsenergy_tot\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase1Outputscalc__energy_tot->clusterphase_preferenceOutputsmin_phase1__calc__energy_tot\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputsforce_max\n", - "\n", - "force_max\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__force_max\n", + "\n", + "calc__force_max\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase1Outputsforce_max->clusterphase_preferenceOutputsforce_max\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase1Outputscalc__force_max->clusterphase_preferenceOutputsmin_phase1__calc__force_max\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputsforces\n", - "\n", - "forces\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__forces\n", + "\n", + "calc__forces\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase1Outputsforces->clusterphase_preferenceOutputsforces\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase1Outputscalc__forces->clusterphase_preferenceOutputsmin_phase1__calc__forces\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputsindices\n", - "\n", - "indices\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__indices\n", + "\n", + "calc__indices\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase1Outputsindices->clusterphase_preferenceOutputsindices\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase1Outputscalc__indices->clusterphase_preferenceOutputsmin_phase1__calc__indices\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputspositions\n", - "\n", - "positions\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__positions\n", + "\n", + "calc__positions\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase1Outputspositions->clusterphase_preferenceOutputspositions\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase1Outputscalc__positions->clusterphase_preferenceOutputsmin_phase1__calc__positions\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputspressures\n", - "\n", - "pressures\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__pressures\n", + "\n", + "calc__pressures\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase1Outputspressures->clusterphase_preferenceOutputspressures\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase1Outputscalc__pressures->clusterphase_preferenceOutputsmin_phase1__calc__pressures\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputssteps\n", - "\n", - "steps\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__steps\n", + "\n", + "calc__steps\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase1Outputssteps->clusterphase_preferenceOutputssteps\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase1Outputscalc__steps->clusterphase_preferenceOutputsmin_phase1__calc__steps\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputstotal_displacements\n", - "\n", - "total_displacements\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__total_displacements\n", + "\n", + "calc__total_displacements\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase1Outputstotal_displacements->clusterphase_preferenceOutputstotal_displacements\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase1Outputscalc__total_displacements->clusterphase_preferenceOutputsmin_phase1__calc__total_displacements\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputsunwrapped_positions\n", - "\n", - "unwrapped_positions\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__unwrapped_positions\n", + "\n", + "calc__unwrapped_positions\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase1Outputsunwrapped_positions->clusterphase_preferenceOutputsunwrapped_positions\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase1Outputscalc__unwrapped_positions->clusterphase_preferenceOutputsmin_phase1__calc__unwrapped_positions\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputsvolume\n", - "\n", - "volume\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__volume\n", + "\n", + "calc__volume\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase1Outputsvolume->clusterphase_preferenceOutputsvolume\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase1Outputscalc__volume->clusterphase_preferenceOutputsmin_phase1__calc__volume\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferencemin_phase2Inputsrun\n", - "\n", - "run\n", + "\n", + "run\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferencemin_phase2Outputsran\n", - "\n", - "ran\n", + "\n", + "ran\n", "\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferencemin_phase2Outputsstructure\n", - "\n", - "structure\n", + "\n", + "structure\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferencecompareInputsstructure2\n", - "\n", - "structure2\n", + "\n", + "structure2\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputsstructure->clusterphase_preferencecompareInputsstructure2\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscells\n", - "\n", - "cells\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__cells\n", + "\n", + "calc__cells\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase2Outputscells->clusterphase_preferenceOutputscells\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputsdisplacements\n", - "\n", - "displacements\n", - "\n", - "\n", + "clusterphase_preferencemin_phase2Outputscalc__cells->clusterphase_preferenceOutputsmin_phase2__calc__cells\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__displacements\n", + "\n", + "calc__displacements\n", + "\n", + "\n", "\n", - "clusterphase_preferencemin_phase2Outputsdisplacements->clusterphase_preferenceOutputsdisplacements\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase2Outputscalc__displacements->clusterphase_preferenceOutputsmin_phase2__calc__displacements\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputsenergy_pot\n", - "\n", - "energy_pot\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputsenergy\n", + "\n", + "energy\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferencecompareInputsenergy2\n", - "\n", - "energy2\n", + "\n", + "energy2\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencemin_phase2Outputsenergy_pot->clusterphase_preferencecompareInputsenergy2\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputsenergy_tot\n", - "\n", - "energy_tot\n", - "\n", - "\n", + "clusterphase_preferencemin_phase2Outputsenergy->clusterphase_preferencecompareInputsenergy2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__energy_tot\n", + "\n", + "calc__energy_tot\n", + "\n", + "\n", "\n", - "clusterphase_preferencemin_phase2Outputsenergy_tot->clusterphase_preferenceOutputsenergy_tot\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputsforce_max\n", - "\n", - "force_max\n", - "\n", - "\n", + "clusterphase_preferencemin_phase2Outputscalc__energy_tot->clusterphase_preferenceOutputsmin_phase2__calc__energy_tot\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__force_max\n", + "\n", + "calc__force_max\n", + "\n", + "\n", "\n", - "clusterphase_preferencemin_phase2Outputsforce_max->clusterphase_preferenceOutputsforce_max\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputsforces\n", - "\n", - "forces\n", - "\n", - "\n", + "clusterphase_preferencemin_phase2Outputscalc__force_max->clusterphase_preferenceOutputsmin_phase2__calc__force_max\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__forces\n", + "\n", + "calc__forces\n", + "\n", + "\n", "\n", - "clusterphase_preferencemin_phase2Outputsforces->clusterphase_preferenceOutputsforces\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputsindices\n", - "\n", - "indices\n", - "\n", - "\n", + "clusterphase_preferencemin_phase2Outputscalc__forces->clusterphase_preferenceOutputsmin_phase2__calc__forces\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__indices\n", + "\n", + "calc__indices\n", + "\n", + "\n", "\n", - "clusterphase_preferencemin_phase2Outputsindices->clusterphase_preferenceOutputsindices\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputspositions\n", - "\n", - "positions\n", - "\n", - "\n", + "clusterphase_preferencemin_phase2Outputscalc__indices->clusterphase_preferenceOutputsmin_phase2__calc__indices\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__positions\n", + "\n", + "calc__positions\n", + "\n", + "\n", "\n", - "clusterphase_preferencemin_phase2Outputspositions->clusterphase_preferenceOutputspositions\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputspressures\n", - "\n", - "pressures\n", - "\n", - "\n", + "clusterphase_preferencemin_phase2Outputscalc__positions->clusterphase_preferenceOutputsmin_phase2__calc__positions\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__pressures\n", + "\n", + "calc__pressures\n", + "\n", + "\n", "\n", - "clusterphase_preferencemin_phase2Outputspressures->clusterphase_preferenceOutputspressures\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputssteps\n", - "\n", - "steps\n", - "\n", - "\n", + "clusterphase_preferencemin_phase2Outputscalc__pressures->clusterphase_preferenceOutputsmin_phase2__calc__pressures\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__steps\n", + "\n", + "calc__steps\n", + "\n", + "\n", "\n", - "clusterphase_preferencemin_phase2Outputssteps->clusterphase_preferenceOutputssteps\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputstotal_displacements\n", - "\n", - "total_displacements\n", - "\n", - "\n", + "clusterphase_preferencemin_phase2Outputscalc__steps->clusterphase_preferenceOutputsmin_phase2__calc__steps\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__total_displacements\n", + "\n", + "calc__total_displacements\n", + "\n", + "\n", "\n", - "clusterphase_preferencemin_phase2Outputstotal_displacements->clusterphase_preferenceOutputstotal_displacements\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputsunwrapped_positions\n", - "\n", - "unwrapped_positions\n", - "\n", - "\n", + "clusterphase_preferencemin_phase2Outputscalc__total_displacements->clusterphase_preferenceOutputsmin_phase2__calc__total_displacements\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__unwrapped_positions\n", + "\n", + "calc__unwrapped_positions\n", + "\n", + "\n", "\n", - "clusterphase_preferencemin_phase2Outputsunwrapped_positions->clusterphase_preferenceOutputsunwrapped_positions\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputsvolume\n", - "\n", - "volume\n", - "\n", - "\n", + "clusterphase_preferencemin_phase2Outputscalc__unwrapped_positions->clusterphase_preferenceOutputsmin_phase2__calc__unwrapped_positions\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__volume\n", + "\n", + "calc__volume\n", + "\n", + "\n", "\n", - "clusterphase_preferencemin_phase2Outputsvolume->clusterphase_preferenceOutputsvolume\n", - "\n", - "\n", - "\n", + "clusterphase_preferencemin_phase2Outputscalc__volume->clusterphase_preferenceOutputsmin_phase2__calc__volume\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferencecompareInputsrun\n", - "\n", - "run\n", + "\n", + "run\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferencecompareOutputsran\n", - "\n", - "ran\n", + "\n", + "ran\n", "\n", "\n", "\n", - "\n", + "\n", "clusterphase_preferencecompareOutputsde\n", - "\n", - "de\n", + "\n", + "de\n", "\n", - "\n", + "\n", "\n", - "clusterphase_preferencecompareOutputsde->clusterphase_preferenceOutputsde\n", - "\n", - "\n", - "\n", + "clusterphase_preferencecompareOutputsde->clusterphase_preferenceOutputscompare__de\n", + "\n", + "\n", + "\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 36, @@ -2822,6 +2967,14 @@ "id": "091e2386-0081-436c-a736-23d019bd9b91", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n" + ] + }, { "name": "stdout", "output_type": "stream", @@ -2953,7 +3106,18 @@ "execution_count": 40, "id": "0dd04b4c-e3e7-4072-ad34-58f2c1e4f596", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to true, andthus could not disconnect from it.\n", + " warn(\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n" + ] + } + ], "source": [ "@Workflow.wrap_as.single_value_node()\n", "def add(a, b):\n", @@ -3030,12 +3194,11 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.499 > 0.2\n", - "0.879 > 0.2\n", - "0.993 > 0.2\n", - "0.606 > 0.2\n", - "0.126 <= 0.2\n", - "Finally 0.126\n" + "0.732 > 0.2\n", + "0.628 > 0.2\n", + "0.220 > 0.2\n", + "0.102 <= 0.2\n", + "Finally 0.102\n" ] } ], From 0094e7d1cc8870ea387606096e6db9a83aba3fbc Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 12:27:21 -0700 Subject: [PATCH 07/97] Rename input channel pull to fetch To avoid a difference in meaning to when a node pulls and actually causes upstream executions --- pyiron_workflow/channels.py | 8 ++++---- pyiron_workflow/node.py | 2 +- tests/unit/test_channels.py | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyiron_workflow/channels.py b/pyiron_workflow/channels.py index 6acdfd5ac..7124c2631 100644 --- a/pyiron_workflow/channels.py +++ b/pyiron_workflow/channels.py @@ -3,10 +3,10 @@ Data channels carry, unsurprisingly, data. Connections are only permissible between opposite sub-types, i.e. input-output. -When input channels `pull()` data in, they set their `value` to the first available +When input channels `fetch()` data in, they set their `value` to the first available data value among their connections -- i.e. the `value` of the first output channel in their connections who has something other than `NotData`. -Input data channels will raise an error if a `pull()` is attempted while their parent +Input data channels will raise an error if a `fetch()` is attempted while their parent node is running. Signal channels are tools for procedurally exposing functionality on nodes. @@ -305,7 +305,7 @@ def to_dict(self) -> dict: class InputData(DataChannel): """ - `pull()` updates input data value to the first available data among connections. + `fetch()` updates input data value to the first available data among connections. The `strict_connections` parameter controls whether connections are subject to type checking requirements. @@ -330,7 +330,7 @@ def __init__( ) self.strict_connections = strict_connections - def pull(self) -> None: + def fetch(self) -> None: """ Sets `value` to the first value among connections that is something other than `NotData`; if no such value exists (e.g. because there are no connections or diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 6243127eb..9493a84bc 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -222,7 +222,7 @@ def process_run_result(self, run_output): def run(self): for inp in self.inputs: - inp.pull() + inp.fetch() return self._run() @manage_status diff --git a/tests/unit/test_channels.py b/tests/unit/test_channels.py index 41567027c..3dd058111 100644 --- a/tests/unit/test_channels.py +++ b/tests/unit/test_channels.py @@ -44,7 +44,7 @@ def test_connections(self): self.assertIn(self.no, self.ni1.connections) self.assertIn(self.ni1, self.no.connections) self.assertNotEqual(self.no.value, self.ni1.value) - self.ni1.pull() + self.ni1.fetch() self.assertEqual(self.no.value, self.ni1.value) with self.subTest("Test disconnection"): @@ -76,7 +76,7 @@ def test_connections(self): with self.subTest("Test iteration"): self.assertTrue(all([con in self.no.connections for con in self.no])) - with self.subTest("Data should update on pull"): + with self.subTest("Data should update on fetch"): self.ni1.disconnect_all() self.no.value = NotData @@ -89,22 +89,22 @@ def test_connections(self): 1, msg="Data should not be getting pushed on connection" ) - self.ni1.pull() + self.ni1.fetch() self.assertEqual( self.ni1.value, 1, msg="NotData values should not be getting pulled" ) self.no.value = 3 - self.ni1.pull() + self.ni1.fetch() self.assertEqual( self.ni1.value, 3, - msg="Data pull should to first connected value that's actually data," + msg="Data fetch should to first connected value that's actually data," "in this case skipping over no_empty" ) self.no_empty.value = 4 - self.ni1.pull() + self.ni1.fetch() self.assertEqual( self.ni1.value, 4, From ee4fd8adf9f46174aaa3f78f62603dab806189f1 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 12:31:34 -0700 Subject: [PATCH 08/97] Create shortcuts to fetch input on IO and Node --- pyiron_workflow/io.py | 4 ++++ pyiron_workflow/node.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pyiron_workflow/io.py b/pyiron_workflow/io.py index 4835a6e5b..20e1ca79c 100644 --- a/pyiron_workflow/io.py +++ b/pyiron_workflow/io.py @@ -194,6 +194,10 @@ def activate_strict_connections(self): def deactivate_strict_connections(self): [c.deactivate_strict_connections() for c in self] + def fetch(self): + for c in self: + c.fetch() + class Outputs(DataIO): @property diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 9493a84bc..753a1aa31 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -221,10 +221,16 @@ def process_run_result(self, run_output): """ def run(self): - for inp in self.inputs: - inp.fetch() + self.fetch_input() return self._run() + def fetch_input(self): + """ + Update input channel values with their current most-prioritized connection's + value. + """ + self.inputs.fetch() + @manage_status def _run(self) -> Any | tuple | Future: """ From c22717d75cb8e3f3163dec85725822ab91051ca2 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 12:39:43 -0700 Subject: [PATCH 09/97] Make the finishing method a run variable --- pyiron_workflow/node.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 753a1aa31..84d0d2ffb 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -222,7 +222,7 @@ def process_run_result(self, run_output): def run(self): self.fetch_input() - return self._run() + return self._run(finished_callback=self.finish_run) def fetch_input(self): """ @@ -232,7 +232,7 @@ def fetch_input(self): self.inputs.fetch() @manage_status - def _run(self) -> Any | tuple | Future: + def _run(self, finished_callback: callable) -> Any | tuple | Future: """ Executes the functionality of the node defined in `on_run`. Handles the status of the node, and communicating with any remote @@ -240,12 +240,12 @@ def _run(self) -> Any | tuple | Future: """ if self.executor is None: run_output = self.on_run(**self.run_args) - return self.finish_run(run_output) + return finished_callback(run_output) else: # Just blindly try to execute -- as we nail down the executor interaction # we'll want to fail more cleanly here. self.future = self.executor.submit(self.on_run, **self.run_args) - self.future.add_done_callback(self.finish_run) + self.future.add_done_callback(finished_callback) return self.future def finish_run(self, run_output: tuple | Future) -> Any | tuple: From 1d7b5c87b1e8ec8865ff642fb5ba4ec72dcca24f Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 12:43:34 -0700 Subject: [PATCH 10/97] Split finishing and emitting `ran` and hint usage in `pull` --- pyiron_workflow/node.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 84d0d2ffb..fe1a1c73c 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -221,6 +221,14 @@ def process_run_result(self, run_output): """ def run(self): + self.fetch_input() + return self._run(finished_callback=self.finish_run_and_emit_ran) + + def pull(self): + raise NotImplementedError + # Need to implement everything for on-the-fly construction of the upstream + # graph and its execution + # Then, self.fetch_input() return self._run(finished_callback=self.finish_run) @@ -265,12 +273,16 @@ def finish_run(self, run_output: tuple | Future) -> Any | tuple: self.running = False try: processed_output = self.process_run_result(run_output) - self.signals.output.ran() return processed_output except Exception as e: self.failed = True raise e + def finish_run_and_emit_ran(self, run_output: tuple | Future) -> Any | tuple: + processed_output = self.finish_run(run_output) + self.signals.output.ran() + return processed_output + def _build_signal_channels(self) -> Signals: signals = Signals() signals.input.run = InputSignal("run", self, self.run) From 9641bc583c2f3dd4703ebc8667ba61fb8b57f7ad Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 12:47:15 -0700 Subject: [PATCH 11/97] Update docstrings --- pyiron_workflow/node.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index fe1a1c73c..dfa94aa7b 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -258,14 +258,9 @@ def _run(self, finished_callback: callable) -> Any | tuple | Future: def finish_run(self, run_output: tuple | Future) -> Any | tuple: """ - Switch the node status, process the run result, then fire the ran signal. + Switch the node status, then process and return the run result. - By extracting this as a separate method, we allow the node to pass the actual - execution off to another entity and release the python process to do other - things. In such a case, this function should be registered as a callback - so that the node can process the results, e.g. by unpacking the futures object, - formatting the results nicely, and/or updating its attributes (like output - channels). + Sets the `failed` status to true if an exception is encountered. """ if isinstance(run_output, Future): run_output = run_output.result() @@ -282,6 +277,10 @@ def finish_run_and_emit_ran(self, run_output: tuple | Future) -> Any | tuple: processed_output = self.finish_run(run_output) self.signals.output.ran() return processed_output + finish_run_and_emit_ran.__doc__ = finish_run.__doc__ + """ + + Finally, fire the `ran` signal. + """ def _build_signal_channels(self) -> Signals: signals = Signals() From 7c3656030e337901c27b511f91005981aa62ff73 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 13:22:01 -0700 Subject: [PATCH 12/97] Always follow the pattern of passing args to on_run And have composite nodes pass just their children (and identify starting nodes) in anticipation of sending composites to an executor --- pyiron_workflow/composite.py | 18 ++++++++++-------- pyiron_workflow/node.py | 2 +- pyiron_workflow/workflow.py | 5 ++--- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 86751dec7..73ca398e5 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -160,13 +160,19 @@ def on_run(self): return self.run_graph @staticmethod - def run_graph(self): - for node in self.starting_nodes: + def run_graph(_nodes: dict[Node], _starting_nodes: list[Node]): + for node in _starting_nodes: node.run() - return DotDict(self.outputs.to_value_dict()) + return _nodes + + @property + def run_args(self) -> dict: + return {"_nodes": self.nodes, "_starting_nodes": self.starting_nodes} def process_run_result(self, run_output): - return run_output + # self.nodes = run_output + # Running on an executor will require a more sophisticated idea than above + return DotDict(self.outputs.to_value_dict()) def disconnect_run(self) -> list[tuple[Channel, Channel]]: """ @@ -252,10 +258,6 @@ def get_data_digraph(self) -> dict[str, set[str]]: return digraph - @property - def run_args(self) -> dict: - return {"self": self} - def _build_io( self, io: Inputs | Outputs, diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index dfa94aa7b..e6623d53a 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -200,11 +200,11 @@ def on_run(self) -> callable[..., Any | tuple]: pass @property + @abstractmethod def run_args(self) -> dict: """ Any data needed for `on_run`, will be passed as **kwargs. """ - return {} @abstractmethod def process_run_result(self, run_output): diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 7177593b7..4ee1be006 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -192,11 +192,10 @@ def inputs(self) -> Inputs: def outputs(self) -> Outputs: return self._build_outputs() - @staticmethod - def run_graph(self): + def run(self): if self.automate_execution: self.set_run_signals_to_dag_execution() - return super().run_graph(self) + return super().run() def to_node(self): """ From 62b987e123ec639bc2ebcc3df17d557744049156 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 13:22:45 -0700 Subject: [PATCH 13/97] Add a method to force the node operation locally with whatever data you currently have --- pyiron_workflow/node.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index e6623d53a..ed7eb5441 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -220,6 +220,15 @@ def process_run_result(self, run_output): run_output: The results of a `self.on_run(self.run_args)` call. """ + @manage_status + def execute(self): + """ + Perform the node's operation with its current data. + + Execution happens directly on this python process. + """ + return self.process_run_result(self.on_run(**self.run_args)) + def run(self): self.fetch_input() return self._run(finished_callback=self.finish_run_and_emit_ran) From 218ad9144377f36c110f2539d1dfaf02b1379aef Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 13:22:50 -0700 Subject: [PATCH 14/97] Add run docstring --- pyiron_workflow/node.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index ed7eb5441..c5cd42c50 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -230,6 +230,16 @@ def execute(self): return self.process_run_result(self.on_run(**self.run_args)) def run(self): + """ + Update the input (with whatever is currently available -- does _not_ trigger + any other nodes to run) and use it to perform the node's operation. + + If executor information is specified, execution happens on that process, a + callback is registered, and futures object is returned. + + Once complete, fire `ran` signal to propagate execution in the computation graph + that owns this node (if any). + """ self.fetch_input() return self._run(finished_callback=self.finish_run_and_emit_ran) From 5ef80f46c1836bc1e02e9a907affe25edb6eed5a Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Thu, 12 Oct 2023 20:38:42 +0000 Subject: [PATCH 15/97] Format black --- pyiron_workflow/function.py | 2 +- pyiron_workflow/node.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index 209b59aac..fa20e52a7 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -492,7 +492,7 @@ def process_run_result(self, function_output: Any | tuple) -> Any | tuple: """ for out, value in zip( self.outputs, - (function_output,) if len(self.outputs) == 1 else function_output + (function_output,) if len(self.outputs) == 1 else function_output, ): out.value = value return function_output diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index c5cd42c50..2ab17d3b1 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -33,6 +33,7 @@ def manage_status(node_method): if the method raises an exception; raises a `RuntimeError` if the node is already `running` or `failed`. """ + def wrapped_method(node: Node, *args, **kwargs): # rather node:Node if node.running: raise RuntimeError(f"{node.label} is already running") @@ -296,10 +297,14 @@ def finish_run_and_emit_ran(self, run_output: tuple | Future) -> Any | tuple: processed_output = self.finish_run(run_output) self.signals.output.ran() return processed_output - finish_run_and_emit_ran.__doc__ = finish_run.__doc__ + """ + + finish_run_and_emit_ran.__doc__ = ( + finish_run.__doc__ + + """ Finally, fire the `ran` signal. """ + ) def _build_signal_channels(self) -> Signals: signals = Signals() From cbf16eeea4d701e12a0a131c115c6f5d0665a444 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 13:46:24 -0700 Subject: [PATCH 16/97] Refactor: slide --- pyiron_workflow/node.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 2ab17d3b1..7b1e091f4 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -259,6 +259,24 @@ def fetch_input(self): """ self.inputs.fetch() + def update_input(self, **kwargs) -> None: + """ + Match keywords to input channel labels and update input values. + + Args: + **kwargs: input label - input value (including channels for connection) + pairs. + """ + for k, v in kwargs.items(): + if k in self.inputs.labels: + self.inputs[k] = v + else: + warnings.warn( + f"The keyword '{k}' was not found among input labels. If you are " + f"trying to update a node keyword, please use attribute assignment " + f"directly instead of calling" + ) + @manage_status def _run(self, finished_callback: callable) -> Any | tuple | Future: """ @@ -356,24 +374,6 @@ def fully_connected(self): and self.signals.fully_connected ) - def update_input(self, **kwargs) -> None: - """ - Match keywords to input channel labels and update input values. - - Args: - **kwargs: input label - input value (including channels for connection) - pairs. - """ - for k, v in kwargs.items(): - if k in self.inputs.labels: - self.inputs[k] = v - else: - warnings.warn( - f"The keyword '{k}' was not found among input labels. If you are " - f"trying to update a node keyword, please use attribute assignment " - f"directly instead of calling" - ) - def __call__(self, **kwargs) -> None: self.update_input(**kwargs) return self.run() From 328b95696f16fa11fdf031c3dafa39d74bf26fed Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 14:01:31 -0700 Subject: [PATCH 17/97] Provide a single interface for updating node input Instead of "update" and "fetch" separately --- pyiron_workflow/node.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 7b1e091f4..f3be97942 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -241,7 +241,7 @@ def run(self): Once complete, fire `ran` signal to propagate execution in the computation graph that owns this node (if any). """ - self.fetch_input() + self.update_input() return self._run(finished_callback=self.finish_run_and_emit_ran) def pull(self): @@ -249,24 +249,24 @@ def pull(self): # Need to implement everything for on-the-fly construction of the upstream # graph and its execution # Then, - self.fetch_input() + self.update_input() return self._run(finished_callback=self.finish_run) - def fetch_input(self): - """ - Update input channel values with their current most-prioritized connection's - value. - """ - self.inputs.fetch() - def update_input(self, **kwargs) -> None: """ - Match keywords to input channel labels and update input values. + Fetch the latest and highest-priority input values from connections, then + overwrite values with keywords arguments matching input channel labels. + + Any channel that has neither a connection nor a kwarg update at time of call is + left unchanged. + + Throws a warning if a keyword is provided that cannot be found among the input + keys. Args: - **kwargs: input label - input value (including channels for connection) - pairs. + **kwargs: input key - input value (including channels for connection) pairs. """ + self.inputs.fetch() for k, v in kwargs.items(): if k in self.inputs.labels: self.inputs[k] = v From 1b90d78cc0119e149bf6a11892ef1232ceb02e8f Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 14:05:40 -0700 Subject: [PATCH 18/97] Add to docstring --- pyiron_workflow/node.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index f3be97942..27509c27c 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -263,6 +263,10 @@ def update_input(self, **kwargs) -> None: Throws a warning if a keyword is provided that cannot be found among the input keys. + If you really want to update just a single value without any other side-effects, + this can always be accomplished by following the full semantic path to the + channel's value: `my_node.input.my_channel.value = "foo"`. + Args: **kwargs: input key - input value (including channels for connection) pairs. """ From 7769270513db287c5880361377700ec25492ae4f Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 14:15:37 -0700 Subject: [PATCH 19/97] Return disconnections when removing nodes --- pyiron_workflow/composite.py | 3 ++- tests/unit/test_workflow.py | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 73ca398e5..bb034bc20 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -361,8 +361,9 @@ def _ensure_node_is_not_duplicated(self, node: Node, label: str): def remove(self, node: Node | str): if isinstance(node, Node): node.parent = None - node.disconnect() + disconnected = node.disconnect() del self.nodes[node.label] + return disconnected else: del self.nodes[node] diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index a169989b9..1c423540b 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -93,10 +93,15 @@ def test_double_workfloage_and_node_removal(self): with self.assertRaises(ValueError): # Can't belong to two workflows at once wf2.add(node2) - wf1.remove(node2) + disconnections = wf1.remove(node2) + self.assertFalse(node2.connected, msg="Removal should first disconnect") + self.assertListEqual( + disconnections, + [(node2.inputs.x, wf1.node1.outputs.y)], + msg="Disconnections should be returned by removal" + ) wf2.add(node2) self.assertEqual(node2.parent, wf2) - self.assertFalse(node2.connected) def test_workflow_io(self): wf = Workflow("wf") From 3b3cfdb317ef99b22eb8d9d5922f2a28f4f21bca Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 14:26:42 -0700 Subject: [PATCH 20/97] :bug: make removing by label do the same thing as by object --- pyiron_workflow/composite.py | 24 ++++++++++++++++-------- tests/unit/test_workflow.py | 13 +++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index bb034bc20..beaed48e2 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -358,14 +358,22 @@ def _ensure_node_is_not_duplicated(self, node: Node, label: str): ) del self.nodes[node.label] - def remove(self, node: Node | str): - if isinstance(node, Node): - node.parent = None - disconnected = node.disconnect() - del self.nodes[node.label] - return disconnected - else: - del self.nodes[node] + def remove(self, node: Node | str) -> list[tuple[Channel, Channel]]: + """ + Remove a node from the `nodes` collection, disconnecting it and setting its + `parent` to None. + + Args: + node (Node|str): The node (or its label) to remove. + + Returns: + [list[tuple[Channel, Channel]]]: Any connections that node had. + """ + node = self.nodes[node] if isinstance(node, str) else node + node.parent = None + disconnected = node.disconnect() + del self.nodes[node.label] + return disconnected def __setattr__(self, key: str, node: Node): if isinstance(node, Node) and key != "parent": diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index 1c423540b..2771eeb99 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -103,6 +103,19 @@ def test_double_workfloage_and_node_removal(self): wf2.add(node2) self.assertEqual(node2.parent, wf2) + node1 = wf1.node1 + disconnections = wf1.remove(node1.label) + self.assertEqual( + node1.parent, + None, + msg="Should be able to remove nodes by label as well as by object" + ) + self.assertListEqual( + [], + disconnections, + msg="node1 should have no connections left" + ) + def test_workflow_io(self): wf = Workflow("wf") wf.create.Function(plus_one, label="n1") From c40a4b1c58aa305127ebcd5cd97db7948e433620 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 15:36:26 -0700 Subject: [PATCH 21/97] Add a method for nodes to copy another node's connections --- pyiron_workflow/node.py | 33 ++++++++++++++++++++++++++++++++- tests/unit/test_function.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 27509c27c..38040df9a 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -20,7 +20,7 @@ import graphviz from pyiron_workflow.composite import Composite - from pyiron_workflow.io import Inputs, Outputs + from pyiron_workflow.io import IO, Inputs, Outputs def manage_status(node_method): @@ -447,3 +447,34 @@ def get_first_shared_parent(self, other: Node) -> Composite | None: our = our.parent their = other return None + + def copy_connections(self, other: Node) -> None: + """ + Copies all the connections in another node to this one. + Expects the channels available on this node to be commensurate to those on the + other, i.e. same label, compatible type hint for the connections that exist. + This node may freely have additional channels not present in the other node. + + If an exception is encountered, any connections copied so far are disconnected + + Args: + other (Node): the node whose connections should be copied. + """ + new_connections = [] + try: + for (my_panel, other_panel) in [ + (self.inputs, other.inputs), + (self.outputs, other.outputs), + (self.signals.input, other.signals.input), + (self.signals.output, other.signals.output), + ]: + for channel in other_panel: + for target in channel.connections: + my_panel[channel.label].connect(target) + new_connections.append((my_panel[channel.label], target)) + except Exception as e: + # If you run into trouble, unwind what you've done + for connection in new_connections: + connection[0].disconnect(connection[1]) + raise e + diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 8aaa9c4b9..8c426e0fd 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -396,6 +396,34 @@ def test_return_value(self): node.run() node.future.result() # Wait for the remote execution to finish + def test_copy_connections(self): + node = Function(plus_one) + + upstream = Function(plus_one) + to_copy = Function(plus_one, x=upstream.outputs.y) + downstream = Function(plus_one, x=to_copy.outputs.y) + upstream > to_copy > downstream + + wrong_io = Function(no_default, x=upstream.outputs.y) + downstream.inputs.x.connect(wrong_io.outputs["x + y + 1"]) + + with self.subTest("Ensure failed copies fail cleanly"): + with self.assertRaises(AttributeError): + node.copy_connections(wrong_io) + self.assertFalse( + node.connected, + msg="The x-input connection should have been copied, but should be " + "removed when the copy fails." + ) + node.disconnect() # Make sure you've got a clean slate + + with self.subTest("Successful copy"): + node.copy_connections(to_copy) + self.assertIn(upstream.outputs.y, node.inputs.x.connections) + self.assertIn(upstream.signals.output.ran, node.signals.input.run) + self.assertIn(downstream.inputs.x, node.outputs.y.connections) + self.assertIn(downstream.signals.input.run, node.signals.output.ran) + @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") class TestSingleValue(unittest.TestCase): From ee4054c55b80a9c095270a2c798088dbd6a8c960 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 15:48:48 -0700 Subject: [PATCH 22/97] Match panel keys, not channel labels --- pyiron_workflow/node.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 38040df9a..debe118cf 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -468,10 +468,10 @@ def copy_connections(self, other: Node) -> None: (self.signals.input, other.signals.input), (self.signals.output, other.signals.output), ]: - for channel in other_panel: + for key, channel in other_panel.items(): for target in channel.connections: - my_panel[channel.label].connect(target) - new_connections.append((my_panel[channel.label], target)) + my_panel[key].connect(target) + new_connections.append((my_panel[key], target)) except Exception as e: # If you run into trouble, unwind what you've done for connection in new_connections: From ef58dc49b20bd50a397a29e5fab6fb29a8eb091d Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 12 Oct 2023 15:57:47 -0700 Subject: [PATCH 23/97] Allow composite children to be replaced by another node As long as that node has no parent or connections, and can successfully copy the connections of the node it is replacing. --- pyiron_workflow/composite.py | 39 +++++++++++++++++++++++- tests/unit/test_macro.py | 58 ++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index beaed48e2..05644d329 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -367,7 +367,7 @@ def remove(self, node: Node | str) -> list[tuple[Channel, Channel]]: node (Node|str): The node (or its label) to remove. Returns: - [list[tuple[Channel, Channel]]]: Any connections that node had. + (list[tuple[Channel, Channel]]): Any connections that node had. """ node = self.nodes[node] if isinstance(node, str) else node node.parent = None @@ -375,6 +375,43 @@ def remove(self, node: Node | str) -> list[tuple[Channel, Channel]]: del self.nodes[node.label] return disconnected + def replace(self, owned_node: Node | str, replacement: Node): + """ + Replaces a node currently owned with a new node instance. + The replacement must not belong to any other parent or have any connections. + The IO of the new node must be a perfect superset of the replaced node, i.e. + channel labels need to match precisely, but additional channels may be present. + After replacement, the new node will have the old node's connections, label, + and belong to this composite. + + Args: + owned_node (Node|str): The node to replace or its label. + replacement (Node): The node to replace it with. + + Returns: + (Node): The node that got removed + """ + if replacement.parent is not None: + raise ValueError( + f"Replacement node must have no parent, but got {replacement.parent}" + ) + if replacement.connected: + raise ValueError("Replacement node must not have any connections") + + if isinstance(owned_node, str): + owned_node = self.nodes[owned_node] + + if owned_node.parent is not self: + raise ValueError( + f"The node being replaced should be a child of this composite, but " + f"another parent was found: {owned_node.parent}" + ) + + replacement.copy_connections(owned_node) + replacement.label = owned_node.label + self.remove(owned_node) + self.add(replacement) + def __setattr__(self, key: str, node: Node): if isinstance(node, Node) and key != "parent": self.add(node, label=key) diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index c928d4be8..d3fb73e0e 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -237,6 +237,64 @@ def only_starting(macro): with self.assertRaises(ValueError): Macro(only_starting) + def test_replace_node(self): + macro = Macro(add_three_macro) + + replacement = Macro( + add_three_macro, + inputs_map={"one__x": "x"}, + outputs_map={"three__result": "result"} + ) + + self.assertEqual( + macro(one__x=0).three__result, + 3, + msg="Sanity check" + ) + + to_replace = macro.two + + macro.replace(to_replace, replacement) + self.assertEqual( + macro(one__x=0).three__result, + 5, + msg="Result should be bigger after replacing an add_one node with an " + "add_three macro" + ) + self.assertFalse( + to_replace.connected, + msg="Replaced node should get disconnected" + ) + self.assertIsNone( + to_replace.parent, + msg="Replaced node should get orphaned" + ) + + another_macro = Macro(add_three_macro) + with self.subTest("Should fail when replacement is connected"): + to_replace.inputs.x = another_macro.outputs.three__result + with self.assertRaises(ValueError): + macro.replace(replacement, to_replace) + to_replace.disconnect() + + with self.subTest("Should fail when replacement has a parent"): + another_macro.add(to_replace, label="extra") + with self.assertRaises(ValueError): + macro.replace(replacement, to_replace) + another_macro.remove(to_replace) + + with self.subTest("Should fail if the node being replaced isn't a child"): + with self.assertRaises(ValueError): + macro.replace(another_macro, to_replace) + + with self.subTest("Should be possible to replace by label too"): + macro.replace("two", to_replace) + self.assertEqual( + macro(one__x=0).three__result, + 3, + msg="Original node should be back in place now" + ) + if __name__ == '__main__': unittest.main() From 7b05afe2936fd133841fedb11f9c0bffe5f5a83b Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 13 Oct 2023 09:16:59 -0700 Subject: [PATCH 24/97] Allow replacing with a compatible class instead of an instance --- pyiron_workflow/composite.py | 25 +++++++++++++++++-------- tests/unit/test_macro.py | 16 ++++++++++++++-- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 05644d329..99593d730 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -375,7 +375,7 @@ def remove(self, node: Node | str) -> list[tuple[Channel, Channel]]: del self.nodes[node.label] return disconnected - def replace(self, owned_node: Node | str, replacement: Node): + def replace(self, owned_node: Node | str, replacement: Node | type[Node]): """ Replaces a node currently owned with a new node instance. The replacement must not belong to any other parent or have any connections. @@ -391,13 +391,6 @@ def replace(self, owned_node: Node | str, replacement: Node): Returns: (Node): The node that got removed """ - if replacement.parent is not None: - raise ValueError( - f"Replacement node must have no parent, but got {replacement.parent}" - ) - if replacement.connected: - raise ValueError("Replacement node must not have any connections") - if isinstance(owned_node, str): owned_node = self.nodes[owned_node] @@ -407,6 +400,22 @@ def replace(self, owned_node: Node | str, replacement: Node): f"another parent was found: {owned_node.parent}" ) + if isinstance(replacement, Node): + if replacement.parent is not None: + raise ValueError( + f"Replacement node must have no parent, but got " + f"{replacement.parent}" + ) + if replacement.connected: + raise ValueError("Replacement node must not have any connections") + elif issubclass(replacement, Node): + replacement = replacement(label=owned_node.label) + else: + raise TypeError( + f"Expected replacement node to be a node instance or node subclass, but " + f"got {replacement}" + ) + replacement.copy_connections(owned_node) replacement.label = owned_node.label self.remove(owned_node) diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index d3fb73e0e..53275c77c 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -287,11 +287,23 @@ def test_replace_node(self): with self.assertRaises(ValueError): macro.replace(another_macro, to_replace) - with self.subTest("Should be possible to replace by label too"): - macro.replace("two", to_replace) + with self.subTest( + "Should be possible to replace with a class instead of an instance" + ): + add_one_class = macro.wrap_as.single_value_node()(add_one) + self.assertTrue(issubclass(add_one_class, SingleValue), msg="Sanity check") + macro.replace(macro.two, add_one_class) self.assertEqual( macro(one__x=0).three__result, 3, + msg="Should be possible to replace with a class instead of an instance" + ) + + with self.subTest("Should be possible to replace by label too"): + macro.replace("two", replacement) + self.assertEqual( + macro(one__x=0).three__result, + 5, msg="Original node should be back in place now" ) From c011e5ba5b527fc72b50f0be90265cb8bd8106df Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 13 Oct 2023 09:31:06 -0700 Subject: [PATCH 25/97] Refactor tests For readability and to make sure subtests don't depend on state that is modified in other (potentially failing) subtests. --- tests/unit/test_macro.py | 94 ++++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 41 deletions(-) diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 53275c77c..b0fd9dd8a 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -240,11 +240,12 @@ def only_starting(macro): def test_replace_node(self): macro = Macro(add_three_macro) - replacement = Macro( + adds_three_node = Macro( add_three_macro, inputs_map={"one__x": "x"}, outputs_map={"three__result": "result"} ) + adds_one_node = macro.two self.assertEqual( macro(one__x=0).three__result, @@ -252,60 +253,71 @@ def test_replace_node(self): msg="Sanity check" ) - to_replace = macro.two + with self.subTest("Verify successful cases"): - macro.replace(to_replace, replacement) - self.assertEqual( - macro(one__x=0).three__result, - 5, - msg="Result should be bigger after replacing an add_one node with an " - "add_three macro" - ) - self.assertFalse( - to_replace.connected, - msg="Replaced node should get disconnected" - ) - self.assertIsNone( - to_replace.parent, - msg="Replaced node should get orphaned" - ) - - another_macro = Macro(add_three_macro) - with self.subTest("Should fail when replacement is connected"): - to_replace.inputs.x = another_macro.outputs.three__result - with self.assertRaises(ValueError): - macro.replace(replacement, to_replace) - to_replace.disconnect() - - with self.subTest("Should fail when replacement has a parent"): - another_macro.add(to_replace, label="extra") - with self.assertRaises(ValueError): - macro.replace(replacement, to_replace) - another_macro.remove(to_replace) - - with self.subTest("Should fail if the node being replaced isn't a child"): - with self.assertRaises(ValueError): - macro.replace(another_macro, to_replace) + macro.replace(adds_one_node, adds_three_node) + self.assertEqual( + macro(one__x=0).three__result, + 5, + msg="Result should be bigger after replacing an add_one node with an " + "add_three macro" + ) + self.assertFalse( + adds_one_node.connected, + msg="Replaced node should get disconnected" + ) + self.assertIsNone( + adds_one_node.parent, + msg="Replaced node should get orphaned" + ) - with self.subTest( - "Should be possible to replace with a class instead of an instance" - ): add_one_class = macro.wrap_as.single_value_node()(add_one) self.assertTrue(issubclass(add_one_class, SingleValue), msg="Sanity check") - macro.replace(macro.two, add_one_class) + macro.replace(adds_three_node, add_one_class) self.assertEqual( macro(one__x=0).three__result, 3, msg="Should be possible to replace with a class instead of an instance" ) - with self.subTest("Should be possible to replace by label too"): - macro.replace("two", replacement) + macro.replace("two", adds_three_node) self.assertEqual( macro(one__x=0).three__result, 5, - msg="Original node should be back in place now" + msg="Should be possible to replace by label" + ) + + with self.subTest("Verify failure cases"): + another_macro = Macro(add_three_macro) + another_node = Macro( + add_three_macro, + inputs_map={"one__x": "x"}, + outputs_map={"three__result": "result"}, ) + another_macro.now_its_a_child = another_node + + with self.assertRaises( + ValueError, + msg="Should fail when replacement has a parent" + ): + macro.replace(macro.two, another_node) + + another_macro.remove(another_node) + another_node.inputs.x = another_macro.outputs.three__result + with self.assertRaises( + ValueError, + msg="Should fail when replacement is connected" + ): + macro.replace(macro.two, another_node) + + another_node.disconnect() + an_ok_replacement = another_macro.two + another_macro.remove(an_ok_replacement) + with self.assertRaises( + ValueError, + msg="Should fail if the node being replaced isn't a child" + ): + macro.replace(another_node, an_ok_replacement) if __name__ == '__main__': From 93728c9226e59046fef29c6e21f3d340bac0c245 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 13 Oct 2023 09:35:40 -0700 Subject: [PATCH 26/97] Update docstring To reflect the fact a class can now be passed --- pyiron_workflow/composite.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 99593d730..24e53ad4a 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -386,7 +386,9 @@ def replace(self, owned_node: Node | str, replacement: Node | type[Node]): Args: owned_node (Node|str): The node to replace or its label. - replacement (Node): The node to replace it with. + replacement (Node | type[Node]): The node or class to replace it with. (If + a class is passed, it has all the same requirements on IO compatibility + and simply gets instantiated.) Returns: (Node): The node that got removed From d24d951f028cb260fa0c05340a968c2277390f3d Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Fri, 13 Oct 2023 16:38:27 +0000 Subject: [PATCH 27/97] Format black --- pyiron_workflow/node.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index debe118cf..0d19102be 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -462,7 +462,7 @@ def copy_connections(self, other: Node) -> None: """ new_connections = [] try: - for (my_panel, other_panel) in [ + for my_panel, other_panel in [ (self.inputs, other.inputs), (self.outputs, other.outputs), (self.signals.input, other.signals.input), @@ -477,4 +477,3 @@ def copy_connections(self, other: Node) -> None: for connection in new_connections: connection[0].disconnect(connection[1]) raise e - From be32f0f04d56f169cd6fe73a6bd4adee27c262d1 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 13 Oct 2023 09:47:25 -0700 Subject: [PATCH 28/97] Add syntactic sugar for triggering replacement from the replacee --- pyiron_workflow/node.py | 17 +++++++++++++++++ tests/unit/test_macro.py | 7 +++++++ 2 files changed, 24 insertions(+) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index debe118cf..e00433468 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -478,3 +478,20 @@ def copy_connections(self, other: Node) -> None: connection[0].disconnect(connection[1]) raise e + def replace_with(self, other: Node | type[Node]): + """ + If this node has a parent, invokes `self.parent.replace(self, other)` to swap + out this node for the other node in the parent graph. + + The replacement must have fully compatible IO, i.e. its IO must be a superset of + this node's IO with all the same labels and type hints (although the latter is + not strictly enforced and will only cause trouble if there is an incompatibility + that causes trouble in the process of copying over connections) + + Args: + other (Node|type[Node]): The replacement. + """ + if self.parent is not None: + self.parent.replace(self, other) + else: + warnings.warn(f"Could not replace {self.label}, as it has no parent.") diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index b0fd9dd8a..33eabd63a 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -287,6 +287,13 @@ def test_replace_node(self): msg="Should be possible to replace by label" ) + macro.two.replace_with(adds_one_node) + self.assertEqual( + macro(one__x=0).three__result, + 3, + msg="Nodes should have syntactic sugar for invoking replacement" + ) + with self.subTest("Verify failure cases"): another_macro = Macro(add_three_macro) another_node = Macro( From 346bcabe91172b55f3f70874fbcb5ef32cac0f96 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 13 Oct 2023 10:00:46 -0700 Subject: [PATCH 29/97] Add syntactic sugar for replacing via class assignment --- pyiron_workflow/composite.py | 7 +++++++ tests/unit/test_macro.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 24e53ad4a..b8677f54a 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -426,6 +426,13 @@ def replace(self, owned_node: Node | str, replacement: Node | type[Node]): def __setattr__(self, key: str, node: Node): if isinstance(node, Node) and key != "parent": self.add(node, label=key) + elif ( + isinstance(node, type) + and issubclass(node, Node) + and key in self.nodes.keys() + ): + # When a class is assigned to an existing node, try a replacement + self.replace(key, node) else: super().__setattr__(key, node) diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 33eabd63a..baeb92583 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -294,6 +294,17 @@ def test_replace_node(self): msg="Nodes should have syntactic sugar for invoking replacement" ) + @Macro.wrap_as.function_node() + def add_two(x): + result = x + 2 + return result + macro.two = add_two + self.assertEqual( + macro(one__x=0).three__result, + 4, + msg="Composite should allow replacement when a class is assigned" + ) + with self.subTest("Verify failure cases"): another_macro = Macro(add_three_macro) another_node = Macro( @@ -326,6 +337,18 @@ def test_replace_node(self): ): macro.replace(another_node, an_ok_replacement) + @Macro.wrap_as.function_node() + def add_two_incompatible_io(not_x): + result_is_not_my_name = not_x + 2 + return result_is_not_my_name + + with self.assertRaises( + AttributeError, + msg="Replacing via class assignment should fail if the class has " + "incompatible IO" + ): + macro.two = add_two_incompatible_io + if __name__ == '__main__': unittest.main() From 3ca64bd2ef132636430d6ce6870f0136294d9ec4 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 13 Oct 2023 10:10:30 -0700 Subject: [PATCH 30/97] :bug: fix docstring example typo --- pyiron_workflow/macro.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index 838ad5782..23216f5d3 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -114,7 +114,7 @@ class Macro(Composite): ... macro.b = macro.create.SingleValue(add_one, x=0) ... macro.c = macro.create.SingleValue(add_one, x=0) >>> - >>> m = Macro(modified_start_macro) + >>> m = Macro(modified_flow_macro) >>> m.outputs.to_value_dict() >>> m(a__x=1, b__x=2, c__x=3) {'a__result': 2, 'b__result': 3, 'c__result': 4} From e540a7811a1bb1148fb544be0ab3d0fcd1f76b2c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 13 Oct 2023 10:34:47 -0700 Subject: [PATCH 31/97] :bug: remove removed nodes from starting_nodes if they're there --- pyiron_workflow/composite.py | 2 ++ tests/unit/test_workflow.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index b8677f54a..4592cd393 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -372,6 +372,8 @@ def remove(self, node: Node | str) -> list[tuple[Channel, Channel]]: node = self.nodes[node] if isinstance(node, str) else node node.parent = None disconnected = node.disconnect() + if node in self.starting_nodes: + self.starting_nodes.remove(node) del self.nodes[node.label] return disconnected diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index 2771eeb99..698cf71d8 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -66,6 +66,26 @@ def test_node_addition(self): with self.assertRaises(AttributeError): Workflow.create.Function(plus_one, label="boa", parent=wf) + def test_node_removal(self): + wf = Workflow("my_workflow") + wf.owned = Workflow.create.Function(plus_one) + node = Workflow.create.Function(plus_one) + wf.foo = node + # Add it to starting nodes manually, otherwise it's only there at run time + wf.starting_nodes = [wf.foo] + # Connect it inside the workflow + wf.foo.inputs.x = wf.owned.outputs.y + + wf.remove(node) + self.assertIsNone(node.parent, msg="Removal should de-parent") + self.assertFalse(node.connected, msg="Removal should disconnect") + self.assertListEqual( + wf.starting_nodes, + [], + msg="Removal should also remove from starting nodes" + ) + + def test_node_packages(self): wf = Workflow("my_workflow") From 7606ab0698fb5be21978b69acd0b810868f1713d Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 13 Oct 2023 10:35:12 -0700 Subject: [PATCH 32/97] If you're replacing a starting node, make the replacement a starter too --- pyiron_workflow/composite.py | 3 +++ tests/unit/test_macro.py | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 4592cd393..b7c5fc787 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -422,8 +422,11 @@ def replace(self, owned_node: Node | str, replacement: Node | type[Node]): replacement.copy_connections(owned_node) replacement.label = owned_node.label + is_starting_node = owned_node in self.starting_nodes self.remove(owned_node) self.add(replacement) + if is_starting_node: + self.starting_nodes.append(replacement) def __setattr__(self, key: str, node: Node): if isinstance(node, Node) and key != "parent": diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index baeb92583..cb4000829 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -305,6 +305,19 @@ def add_two(x): msg="Composite should allow replacement when a class is assigned" ) + self.assertListEqual( + macro.starting_nodes, + [macro.one], + msg="Sanity check" + ) + new_starter = add_two() + macro.one.replace_with(new_starter) + self.assertListEqual( + macro.starting_nodes, + [new_starter], + msg="Replacement should be reflected in the starting nodes" + ) + with self.subTest("Verify failure cases"): another_macro = Macro(add_three_macro) another_node = Macro( From bb69bc34e6d35ad02745efdf073d522877dfaa88 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 13 Oct 2023 10:47:24 -0700 Subject: [PATCH 33/97] Make sure Macros update their IO on replacement For Workflows IO is built on-the-fly, so it's a non-issue --- pyiron_workflow/macro.py | 13 +++++++++++++ tests/unit/test_macro.py | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index 23216f5d3..7339483df 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -14,6 +14,8 @@ if TYPE_CHECKING: from bidict import bidict + from pyiron_workflow.node import Node + class Macro(Composite): """ @@ -169,6 +171,10 @@ def inputs(self) -> Inputs: def outputs(self) -> Outputs: return self._outputs + def _rebuild_data_io(self): + self._inputs = self._build_inputs() + self._outputs = self._build_outputs() + def _configure_graph_execution(self): run_signals = self.disconnect_run() @@ -195,6 +201,13 @@ def _reconnect_run(self, run_signal_pairs_to_restore): for pairs in run_signal_pairs_to_restore: pairs[0].connect(pairs[1]) + def replace(self, owned_node: Node | str, replacement: Node | type[Node]): + super().replace(owned_node=owned_node, replacement=replacement) + # Make sure node-level IO is pointing to the new node + self._rebuild_data_io() + # This is brute-force overkill since only the replaced node needs to be updated + # but it's not particularly expensive + def to_workfow(self): raise NotImplementedError diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index cb4000829..b8d811339 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -317,6 +317,11 @@ def add_two(x): [new_starter], msg="Replacement should be reflected in the starting nodes" ) + self.assertIs( + macro.inputs.one__x, + new_starter.inputs.x, + msg="Replacement should be reflected in composite IO" + ) with self.subTest("Verify failure cases"): another_macro = Macro(add_three_macro) From d7f280cc61e7ec5941dd59317b84851491b5652c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 13 Oct 2023 10:47:49 -0700 Subject: [PATCH 34/97] Update macro docstring --- pyiron_workflow/macro.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index 7339483df..cdea3244a 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -135,6 +135,27 @@ class Macro(Composite): Manually controlling execution flow is necessary for cyclic graphs (cf. the while loop meta-node), but best to avoid when possible as it's easy to miss intended connections in complex graphs. + + We can also modify an existing macro at runtime by replacing nodes within it, as + long as the replacement has fully compatible IO. There are three syntacic ways + to do this. Let's explore these by going back to our `add_three_macro` and + replacing each of its children with a node that adds 2 instead of 1. + >>> @Macro.wrap_as.single_value_node() + ... def add_two(x): + ... result = x + 2 + ... return result + >>> + >>> adds_six_macro = Macro(add_three_macro) + >>> # With the replace method + >>> # (replacement target can be specified by label or instance, + >>> # the replacing node can be specified by instance or class) + >>> adds_six_macro.replace(adds_six_macro.one, add_two()) + >>> # With the replace_with method + >>> adds_six_macro.two.replace_with(add_two()) + >>> # And by assignment of a compatible class to an occupied node label + >>> adds_six_macro.three = add_two + >>> adds_six_macro(inp=1) + {'three__result': 7} """ def __init__( From de0699af01ff64ce68c6fa3215fd01058c6bfe9a Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 13 Oct 2023 11:06:08 -0700 Subject: [PATCH 35/97] Update the demo notebook --- notebooks/workflow_example.ipynb | 135 ++++++++++++++++++++++++++----- 1 file changed, 117 insertions(+), 18 deletions(-) diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index 727fc7de1..467ee1d49 100644 --- a/notebooks/workflow_example.ipynb +++ b/notebooks/workflow_example.ipynb @@ -634,8 +634,8 @@ { "data": { "text/plain": [ - "array([0.23888392, 0.24728969, 0.2937081 , 0.66902714, 0.47716927,\n", - " 0.12469378, 0.36244931, 0.01119268, 0.26370445, 0.84121862])" + "array([0.98830723, 0.76898713, 0.05580742, 0.70040889, 0.59265983,\n", + " 0.56839124, 0.48762346, 0.90402605, 0.86155979, 0.68738559])" ] }, "execution_count": 22, @@ -644,7 +644,7 @@ }, { "data": { - "image/png": "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IoyMjIxIkrKysgxXAgAA7BoZGZHb7Z7x7w7rQnHlEjAxMaHTp08rJSVFDof9B5kNDw8rKytLvb29Sk1NnYcKMdcYs9jDmMUWxiv2xOKYWZalkZERXXHFFUpImPnKkJiYGUlISNDy5csv+n1SU1NjZgBxBmMWexiz2MJ4xZ5YG7PzzYhM4gJWAABgFGEEAAAYFRdhJDk5WTU1NUpOTjZdCmaJMYs9jFlsYbxiz0Ies5i4gBUAACxccTEzAgAALl2EEQAAYBRhBAAAGEUYAQAARi2YMFJfX6+cnBy5XC55vV61tLSct/8777wjr9crl8ulq6++Wt/+9rejVCkm2RmzN998U+vXr9dnPvMZpaamqqSkRD/60Y+iWC3s/hub9O677yoxMVE33HDD/BaIMHbHbGxsTLt371Z2draSk5O1cuVK7d+/P0rVQrI/ZocOHdL111+vyy67TB6PR/fcc48GBwejVO0cshaA7373u9aiRYusF154werq6rIefPBBa8mSJdb//d//Tdv/xIkT1mWXXWY9+OCDVldXl/XCCy9YixYtsl5//fUoVx6/7I7Zgw8+aD355JNWW1ub9f7771vV1dXWokWLrGPHjkW58vhkd7wm/epXv7Kuvvpqq6yszLr++uujUywsy4pszL7whS9YxcXFls/ns7q7u63/+q//st59990oVh3f7I5ZS0uLlZCQYD377LPWiRMnrJaWFmvNmjXWpk2bolz5xVsQYaSoqMiqrKwMabv22mutRx99dNr+f/u3f2tde+21IW333nuvdeONN85bjQhld8ymk5eXZ9XW1s51aZhGpONVUVFh/f3f/71VU1NDGIkyu2P2wx/+0HK73dbg4GA0ysM07I7Z17/+devqq68OafvmN79pLV++fN5qnC8xv0wzPj6u9vZ2lZWVhbSXlZWptbV12nN+8pOfhPX//Oc/r6NHj+qTTz6Zt1pxRiRjdq6JiQmNjIwoLS1tPkrEWSIdr5deekkffvihampq5rtEnCOSMfvBD36gwsJCfe1rX9OVV16pa665Rg8//LB++9vfRqPkuBfJmJWWlurkyZNqamqSZVn66KOP9Prrr+uOO+6IRslzKiYelHc+AwMDCgaDysjICGnPyMhQX1/ftOf09fVN2//TTz/VwMCAPB7PvNWLyMbsXE899ZR+85vf6M4775yPEnGWSMbrgw8+0KOPPqqWlhYlJsb8f2ZiTiRjduLECR05ckQul0vf+973NDAwoK985SsaGhriupEoiGTMSktLdejQIVVUVGh0dFSffvqpvvCFL+hb3/pWNEqeUzE/MzLJ4XCEvLYsK6ztQv2na8f8sTtmk1599VU9/vjjamxs1LJly+arPJxjtuMVDAZ11113qba2Vtdcc020ysM07Pwbm5iYkMPh0KFDh1RUVKTbb79dTz/9tF5++WVmR6LIzph1dXXpgQce0GOPPab29nY1Nzeru7tblZWV0Sh1TsX8/7Kkp6fL6XSGJcf+/v6whDkpMzNz2v6JiYlaunTpvNWKMyIZs0mNjY3avn27XnvtNd12223zWSZ+x+54jYyM6OjRo+ro6ND9998v6cwPnWVZSkxM1FtvvaVbbrklKrXHq0j+jXk8Hl155ZUhj3vPzc2VZVk6efKkVq1aNa81x7tIxqyurk5r167VI488Ikm67rrrtGTJEq1bt05PPPFETM3yx/zMSFJSkrxer3w+X0i7z+dTaWnptOeUlJSE9X/rrbdUWFioRYsWzVutOCOSMZPOzIhs27ZNr7zySkyuicYqu+OVmpqqn/70pzp+/PjUUVlZqdWrV+v48eMqLi6OVulxK5J/Y2vXrtXp06f161//eqrt/fffV0JCgpYvXz6v9SKyMfv444+VkBD6M+50OiX9/9n+mGHqytm5NHk71L59+6yuri6rqqrKWrJkifWLX/zCsizLevTRR60tW7ZM9Z+8tfehhx6yurq6rH379nFrb5TZHbNXXnnFSkxMtJ577jnL7/dPHb/61a9MfYW4Yne8zsXdNNFnd8xGRkas5cuXW3/6p39q/exnP7Peeecda9WqVdaOHTtMfYW4Y3fMXnrpJSsxMdGqr6+3PvzwQ+vIkSNWYWGhVVRUZOorRGxBhBHLsqznnnvOys7OtpKSkqyCggLrnXfemfrb3Xffbd10000h/d9++23rs5/9rJWUlGStWLHCamhoiHLFsDNmN910kyUp7Lj77rujX3icsvtv7GyEETPsjtl7771n3XbbbdbixYut5cuXW7t27bI+/vjjKFcd3+yO2Te/+U0rLy/PWrx4seXxeKy/+Iu/sE6ePBnlqi+ew7JibS4HAAAsJDF/zQgAAIhthBEAAGAUYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABG/T8nSJ5zOJcZ9gAAAABJRU5ErkJggg==", + "image/png": "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ioqKBu2v3bt3a86cORo3btzlP0FYToNFVOm/pG7z5s3G4/GY1atXm/Hjx5v//ve/xhhj1q1bZ+67776B+a+//rpJSkoyL730kvF6vQO3L774wtZLiHrB7oOLcTXN5Qn2/ff7/WbatGnm5z//ufnoo4/Mnj17zMyZM81DDz1k6yVEvWD3wdatW01SUpKprq42J06cMPv27TNz5swxhYWFtl5CVPP7/aapqck0NTUZSWbTpk2mqalp4NLqi9///kt716xZYzwej9m8eTOX9uLyvfTSSyYrK8skJyeb/Px8s2fPnoGf3X///ebWW28duH/rrbcaSYNu999/f+QLjyHB7IOLEUYuX7Dv//Hjx83tt99urrjiCjNt2jSzdu1a8/XXX0e46tgS7D54/vnnTW5urrniiiuM2+02v/jFL8zp06cjXHVs+Oc//3nJ3+tDvf//+te/zM0332ySk5PNtddea2pqasJWj8MYjm8BAAB7OGcEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABg1f8C2I7ZC9TwoakAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -1199,7 +1199,7 @@ "
\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 28, @@ -1230,7 +1230,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9dbe327786644b2ca0ea31216fc48bc9", + "model_id": "5276705f13934382a5419737139590e7", "version_major": 2, "version_minor": 0 }, @@ -1257,7 +1257,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 29, @@ -1509,7 +1509,7 @@ "
\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 30, @@ -2928,7 +2928,7 @@ "
\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 36, @@ -2990,6 +2990,103 @@ "print(f\"{wf.inputs.element.value}: E({wf.inputs.phase2.value}) - E({wf.inputs.phase1.value}) = {out.compare__de:.2f} eV/atom\")" ] }, + { + "cell_type": "markdown", + "id": "7985d84f-b842-4a8d-95d5-eca3d19a78c8", + "metadata": {}, + "source": [ + "We can also replace entire node in a workflow or macro with a new node, booting the old one out and inserting the new one including all its connections. Because the connections are recreated, the replacement node _must_ have compatible IO to the node being replaced.\n", + "\n", + "There are several syntacic approaches for doing this, including invoking replacement methods from the workflow (or macro) or from the node being replaced, or a new (compatible!) class can be assigned directly to an existing node. We'll use the last approach, which makes a new instance of the supplied class and replaces the target node with it.\n", + "\n", + "Let's replace the calculation type for phase 1 -- let's switch it from a `CalcMin` to a `CalcStatic`; both of these take a `job` as input and give `structure` and `energy` as output, so we won't have any trouble connecting our new node in lieue of the old one." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "4cdffdca-48d3-4486-9045-48102c7e5f31", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel job was not connected to job, andthus could not disconnect from it.\n", + " warn(\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel energy_pot was not connected to energy1, andthus could not disconnect from it.\n", + " warn(\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n" + ] + } + ], + "source": [ + "wf.min_phase1.calc = Macro.create.atomistics.CalcStatic" + ] + }, + { + "cell_type": "markdown", + "id": "8dd7d2f9-313d-4e38-b467-823c48d0afa0", + "metadata": {}, + "source": [ + "Since we're no longer allowing our first phase to relax while the second phase still can, we would expect the second phase to have a much lower energy than the first one. If our lattice guess for the first phase is bad enough, this could even switch the preferred phase!\n", + "\n", + "Look at Al's fcc-hcp energy difference using this new workflow. We'll always let hcp relax, but freeze the fcc cell so we can see the impact of a good and bad lattice guess." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "ed4a3a22-fc3a-44c9-9d4f-c65bc1288889", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The job JUSTAJOBNAME was saved and received the ID: 9558\n", + "The job JUSTAJOBNAME was saved and received the ID: 9558\n", + "Al: E(hcp) - E(fcc) = -5.57 eV/atom\n" + ] + } + ], + "source": [ + "# Bad guess\n", + "out = wf(element=\"Al\", phase1=\"fcc\", phase2=\"hcp\", lattice_guess1=3, lattice_guess2=3)\n", + "print(f\"{wf.inputs.element.value}: E({wf.inputs.phase2.value}) - E({wf.inputs.phase1.value}) = {out.compare__de:.2f} eV/atom\")" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "5a985cbf-c308-4369-9223-b8a37edb8ab1", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The job JUSTAJOBNAME was saved and received the ID: 9558\n", + "The job JUSTAJOBNAME was saved and received the ID: 9558\n", + "Al: E(hcp) - E(fcc) = 0.03 eV/atom\n" + ] + } + ], + "source": [ + "# Good guess\n", + "out = wf(element=\"Al\", phase1=\"fcc\", phase2=\"hcp\", lattice_guess1=4.05, lattice_guess2=3)\n", + "print(f\"{wf.inputs.element.value}: E({wf.inputs.phase2.value}) - E({wf.inputs.phase1.value}) = {out.compare__de:.2f} eV/atom\")" + ] + }, { "cell_type": "markdown", "id": "f447531e-3e8c-4c7e-a579-5f9c56b75a5b", @@ -3052,7 +3149,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 42, "id": "0b373764-b389-4c24-8086-f3d33a4f7fd7", "metadata": {}, "outputs": [ @@ -3066,7 +3163,7 @@ " 17.230249999999995]" ] }, - "execution_count": 39, + "execution_count": 42, "metadata": {}, "output_type": "execute_result" } @@ -3103,7 +3200,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 43, "id": "0dd04b4c-e3e7-4072-ad34-58f2c1e4f596", "metadata": {}, "outputs": [ @@ -3162,7 +3259,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 44, "id": "2dfb967b-41ac-4463-b606-3e315e617f2a", "metadata": {}, "outputs": [ @@ -3186,7 +3283,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 45, "id": "2e87f858-b327-4f6b-9237-c8a557f29aeb", "metadata": {}, "outputs": [ @@ -3194,11 +3291,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.732 > 0.2\n", - "0.628 > 0.2\n", - "0.220 > 0.2\n", - "0.102 <= 0.2\n", - "Finally 0.102\n" + "0.710 > 0.2\n", + "0.757 > 0.2\n", + "0.651 > 0.2\n", + "0.821 > 0.2\n", + "0.677 > 0.2\n", + "0.189 <= 0.2\n", + "Finally 0.189\n" ] } ], From 43f37b56a87d92f250c822a54ff25a8983a7c239 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 13 Oct 2023 12:17:08 -0700 Subject: [PATCH 36/97] Allow the function node to be a method And set it dynamically using the decorators. This way it can be used from the class level if you want to extend/exploit the behaviour of an existing function node. --- notebooks/workflow_example.ipynb | 1778 ++---------------------------- pyiron_workflow/function.py | 53 +- tests/unit/test_function.py | 17 + 3 files changed, 128 insertions(+), 1720 deletions(-) diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index 467ee1d49..77fdca1df 100644 --- a/notebooks/workflow_example.ipynb +++ b/notebooks/workflow_example.ipynb @@ -411,13 +411,13 @@ "text": [ "class name = SubtractNode\n", "label = subtract_node\n", - "default output = 1\n" + "default output = -1\n" ] } ], "source": [ "@function_node(\"diff\")\n", - "def subtract_node(x: int | float = 2, y: int | float = 1) -> int | float:\n", + "def subtract_node(x: int | float = 1, y: int | float = 2) -> int | float:\n", " return x - y\n", "\n", "sn = subtract_node()\n", @@ -428,6 +428,38 @@ "print(\"default output =\", sn.outputs.diff.value)" ] }, + { + "cell_type": "markdown", + "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:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "b8c845b7-7088-43d7-b106-7a6ba1c571ec", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "default output = 1\n" + ] + } + ], + "source": [ + "@function_node(\"square_diff\")\n", + "def subtract_and_sqaure_node(x: int | float = 1, y: int | float = 2) -> int | float:\n", + " return subtract_node.node_function(x, y)**2\n", + " \n", + "ssq = subtract_and_sqaure_node()\n", + "ssq()\n", + "print(\"default output =\", ssq.outputs.square_diff.value)" + ] + }, { "cell_type": "markdown", "id": "9b9220b0-833d-4c6a-9929-5dfa60a47d14", @@ -450,7 +482,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 18, "id": "2e418abf-7059-4e1e-9b9f-b3dc0a4b5e35", "metadata": { "tags": [] @@ -500,7 +532,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "id": "59c29856-c77e-48a1-9f17-15d4c58be588", "metadata": {}, "outputs": [ @@ -536,7 +568,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, "id": "1a4e9693-0980-4435-aecc-3331d8b608dd", "metadata": {}, "outputs": [], @@ -548,7 +580,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 21, "id": "7c4d314b-33bb-4a67-bfb9-ed77fba3949c", "metadata": {}, "outputs": [ @@ -587,7 +619,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "id": "61ae572f-197b-4a60-8d3e-e19c1b9cc6e2", "metadata": {}, "outputs": [ @@ -627,24 +659,24 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "id": "6569014a-815b-46dd-8b47-4e1cd4584b3b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "array([0.98830723, 0.76898713, 0.05580742, 0.70040889, 0.59265983,\n", - " 0.56839124, 0.48762346, 0.90402605, 0.86155979, 0.68738559])" + "array([0.87397076, 0.3156643 , 0.76573646, 0.6628496 , 0.98520977,\n", + " 0.66537535, 0.77108864, 0.64995335, 0.6862261 , 0.43859525])" ] }, - "execution_count": 22, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiMAAAGdCAYAAADAAnMpAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAg20lEQVR4nO3de2zV9f3H8ddpS3uU0WMK0h6g1sJAWxvRlhRbRtycVNDUH8kWahyiDhOLc9ymC4zFWmLW6CZxXlqdcskCssbrJOkq/WODcpkMaBPxkGigsyCnNm3j6fFSkPbz+4Nf+/PQFnsOp+fTc87zkZyQ8+nne877nG9oX+f7+X7fx2GMMQIAALAkwXYBAAAgvhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFiVZLuAkejr69OZM2c0YcIEORwO2+UAAIARMMbI7/drypQpSkgY/vhHVISRM2fOKDMz03YZAAAgBKdOndK0adOG/XlUhJEJEyZIuvBiUlNTLVcDAABGoru7W5mZmQN/x4cTFWGkf2kmNTWVMAIAQJT5vlMsOIEVAABYRRgBAABWEUYAAIBVhBEAAGBV0GFk7969Ki0t1ZQpU+RwOPTuu+9+7zZ79uxRQUGBnE6npk+frpdffjmUWgEAQAwKOox89dVXmj17tl588cURzW9padGdd96p+fPnq6mpSb/73e+0cuVKvfXWW0EXCwAAYk/Ql/YuWrRIixYtGvH8l19+Wddcc42ee+45SVJOTo4OHz6sP/3pT/rZz34W7NMDAIAYM+rnjBw8eFAlJSUBY3fccYcOHz6sb7/9dshtzp49q+7u7oAbAACITaMeRtra2pSenh4wlp6ervPnz6ujo2PIbaqqquRyuQZutIIHACD8evuMDp7o1N+bP9PBE53q7TNW6ohIB9aLO68ZY4Yc77d+/XqtXbt24H5/O1kAABAe9ce8qtzlkdfXMzDmdjlVUZqrhXnuiNYy6kdGMjIy1NbWFjDW3t6upKQkTZw4cchtUlJSBlq/0wIeAIDwqj/m1YrtRwOCiCS1+Xq0YvtR1R/zRrSeUQ8jRUVFamhoCBjbvXu35syZo3Hjxo320wMAgO/o7TOq3OXRUAsy/WOVuzwRXbIJOox8+eWXam5uVnNzs6QLl+42NzertbVV0oUllmXLlg3MLy8v16effqq1a9fq+PHj2rJlizZv3qzHHnssPK8AAACM2KGWrkFHRL7LSPL6enSopStiNQV9zsjhw4f1k5/8ZOB+/7kd999/v7Zt2yav1zsQTCQpOztbdXV1WrNmjV566SVNmTJFzz//PJf1AgBgQbt/+CASyrxwCDqM/PjHPx44AXUo27ZtGzR266236ujRo8E+FQAACLPJE5xhnRcOfDcNAABxpDA7TW6XU0Nfzyo5dOGqmsLstIjVRBgBACCOJCY4VFGaK0mDAkn//YrSXCUmDBdXwo8wAgBAnFmY51bN0nxluAKXYjJcTtUszY94n5GIND0DAABjy8I8txbkZuhQS5fa/T2aPOHC0kwkj4j0I4wAABCnEhMcKpoxdAPSSGKZBgAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYlWS7AAAAYlVvn9Ghli61+3s0eYJThdlpSkxw2C5rzCGMAAAwCuqPeVW5yyOvr2dgzO1yqqI0Vwvz3BYrG3tYpgEAIMzqj3m1YvvRgCAiSW2+Hq3YflT1x7yWKhubCCMAAIRRb59R5S6PzBA/6x+r3OVRb99QM+ITYQQAgDA61NI16IjIdxlJXl+PDrV0Ra6oMY4wAgBAGLX7hw8iocyLB4QRAADCaPIEZ1jnxQPCCAAAYVSYnSa3y6nhLuB16MJVNYXZaZEsa0wjjAAAEEaJCQ5VlOZK0qBA0n+/ojSXfiPfQRgBACDMFua5VbM0XxmuwKWYDJdTNUvz6TNyEZqeAQAwChbmubUgN4MOrCNAGAEQ92jZjdGSmOBQ0YyJtssY8wgjAOIaLbsB+zhnBEDcomU3MDYQRgDEJVp2A2MHYQRAXKJlNzB2EEYAxCVadgNjR0hhpLq6WtnZ2XI6nSooKFBjY+Ml5+/YsUOzZ8/WlVdeKbfbrQcffFCdnZ0hFQwA4UDLbmDsCDqM1NbWavXq1dqwYYOampo0f/58LVq0SK2trUPO37dvn5YtW6bly5fro48+0htvvKH//Oc/euihhy67eAAIFS27gbEj6DCyadMmLV++XA899JBycnL03HPPKTMzUzU1NUPO//e//61rr71WK1euVHZ2tn70ox/p4Ycf1uHDhy+7eAAIFS27gbEjqDBy7tw5HTlyRCUlJQHjJSUlOnDgwJDbFBcX6/Tp06qrq5MxRp9//rnefPNN3XXXXcM+z9mzZ9Xd3R1wA4Bwo2U3MDYE1fSso6NDvb29Sk9PDxhPT09XW1vbkNsUFxdrx44dKisrU09Pj86fP6+7775bL7zwwrDPU1VVpcrKymBKA4CQ0LIbsC+kE1gdjsD/pMaYQWP9PB6PVq5cqSeeeEJHjhxRfX29WlpaVF5ePuzjr1+/Xj6fb+B26tSpUMoEgBHpb9n9PzdNVdGMiQQRIMKCOjIyadIkJSYmDjoK0t7ePuhoSb+qqirNmzdPjz/+uCTpxhtv1Pjx4zV//nw99dRTcrsHHwZNSUlRSkpKMKUBAIAoFdSRkeTkZBUUFKihoSFgvKGhQcXFxUNu8/XXXyshIfBpEhMTJV04ogIAAOJb0Ms0a9eu1WuvvaYtW7bo+PHjWrNmjVpbWweWXdavX69ly5YNzC8tLdXbb7+tmpoanTx5Uvv379fKlStVWFioKVOmhO+VAACAqBT0t/aWlZWps7NTGzdulNfrVV5enurq6pSVlSVJ8nq9AT1HHnjgAfn9fr344ov6zW9+o6uuukq33Xabnn766fC9CgAALlNvn+FEZkscJgrWSrq7u+VyueTz+ZSammq7HABAjKk/5lXlLk/A9xW5XU5VlOZyifdlGOnfb76bBgAQ1+qPebVi+9FBX5zY5uvRiu1HVX/Ma6my+EEYAQDErd4+o8pdHg21RNA/VrnLo96+Mb+IENUIIwCAuHWopWvQEZHvMpK8vh4daumKXFFxiDACAIhb7f7hg0go8xAawggAIG5NnuD8/klBzENoCCMAgLhVmJ0mt8s56Jub+zl04aqawuy0SJYVdwgjAIC4lZjgUEVpriQNCiT99ytKc+k3MsoIIwCAuLYwz62apfnKcAUuxWS4nKpZmk+fkQgIugMrAACxZmGeWwtyM+jAaglhBAAAXViyKZox0XYZcYllGgAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVSGFkerqamVnZ8vpdKqgoECNjY2XnH/27Flt2LBBWVlZSklJ0YwZM7Rly5aQCgYAALElKdgNamtrtXr1alVXV2vevHl65ZVXtGjRInk8Hl1zzTVDbrNkyRJ9/vnn2rx5s374wx+qvb1d58+fv+ziAQBA9HMYY0wwG8ydO1f5+fmqqakZGMvJydHixYtVVVU1aH59fb3uuecenTx5UmlpaSEV2d3dLZfLJZ/Pp9TU1JAeAwAARNZI/34HtUxz7tw5HTlyRCUlJQHjJSUlOnDgwJDbvPfee5ozZ46eeeYZTZ06VbNmzdJjjz2mb775ZtjnOXv2rLq7uwNuAAAgNgW1TNPR0aHe3l6lp6cHjKenp6utrW3IbU6ePKl9+/bJ6XTqnXfeUUdHhx555BF1dXUNe95IVVWVKisrgykNAABEqZBOYHU4HAH3jTGDxvr19fXJ4XBox44dKiws1J133qlNmzZp27Ztwx4dWb9+vXw+38Dt1KlToZQJAACiQFBHRiZNmqTExMRBR0Ha29sHHS3p53a7NXXqVLlcroGxnJwcGWN0+vRpzZw5c9A2KSkpSklJCaY0AAAQpYI6MpKcnKyCggI1NDQEjDc0NKi4uHjIbebNm6czZ87oyy+/HBj7+OOPlZCQoGnTpoVQMgAAiCVBL9OsXbtWr732mrZs2aLjx49rzZo1am1tVXl5uaQLSyzLli0bmH/vvfdq4sSJevDBB+XxeLR37149/vjj+uUvf6krrrgifK8EAABEpaD7jJSVlamzs1MbN26U1+tVXl6e6urqlJWVJUnyer1qbW0dmP+DH/xADQ0N+vWvf605c+Zo4sSJWrJkiZ566qnwvQoAABC1gu4zYgN9RgAAiD6j0mcEAAAg3AgjAADAKsIIAACwijACAACsIowAAACrgr60FwDGut4+o0MtXWr392jyBKcKs9OUmDD0V1YAsI8wAiCm1B/zqnKXR15fz8CY2+VURWmuFua5LVYGYDgs0wCIGfXHvFqx/WhAEJGkNl+PVmw/qvpjXkuVAbgUwgiAmNDbZ1S5y6Ohujj2j1Xu8qi3b8z3eQTiDmEEQEw41NI16IjIdxlJXl+PDrV0Ra4oACNCGAEQE9r9wweRUOYBiBzCCICYMHmCM6zzAEQOYQRATCjMTpPb5dRwF/A6dOGqmsLstEiWBWAECCMAYkJigkMVpbmSNCiQ9N+vKM2l3wgwBhFGAMSMhXlu1SzNV4YrcCkmw+VUzdJ8+owAYxRNzwDElIV5bi3IzaADKxBFCCMAYk5igkNFMybaLgPACLFMAwAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKtCCiPV1dXKzs6W0+lUQUGBGhsbR7Td/v37lZSUpJtuuimUpwUAADEo6DBSW1ur1atXa8OGDWpqatL8+fO1aNEitba2XnI7n8+nZcuW6ac//WnIxQIAgNjjMMaYYDaYO3eu8vPzVVNTMzCWk5OjxYsXq6qqatjt7rnnHs2cOVOJiYl699131dzcPOLn7O7ulsvlks/nU2pqajDlAgAAS0b69zuoIyPnzp3TkSNHVFJSEjBeUlKiAwcODLvd1q1bdeLECVVUVIzoec6ePavu7u6AGwAAiE1BhZGOjg719vYqPT09YDw9PV1tbW1DbvPJJ59o3bp12rFjh5KSkkb0PFVVVXK5XAO3zMzMYMoEAABRJKQTWB0OR8B9Y8ygMUnq7e3Vvffeq8rKSs2aNWvEj79+/Xr5fL6B26lTp0IpEwAARIGRHar4P5MmTVJiYuKgoyDt7e2DjpZIkt/v1+HDh9XU1KRHH31UktTX1ydjjJKSkrR7927ddtttg7ZLSUlRSkpKMKUBAIAoFdSRkeTkZBUUFKihoSFgvKGhQcXFxYPmp6am6sMPP1Rzc/PArby8XNddd52am5s1d+7cy6seAABEvaCOjEjS2rVrdd9992nOnDkqKirSX/7yF7W2tqq8vFzShSWWzz77TH/961+VkJCgvLy8gO0nT54sp9M5aBwAAMSnoMNIWVmZOjs7tXHjRnm9XuXl5amurk5ZWVmSJK/X+709RwAAAPoF3WfEBvqMAAAQfUalzwgAAEC4EUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGBVSGGkurpa2dnZcjqdKigoUGNj47Bz3377bS1YsEBXX321UlNTVVRUpPfffz/kggEAl9bbZ3TwRKf+3vyZDp7oVG+fsV0ScElJwW5QW1ur1atXq7q6WvPmzdMrr7yiRYsWyePx6Jprrhk0f+/evVqwYIH+8Ic/6KqrrtLWrVtVWlqqDz74QDfffHNYXgQA4IL6Y15V7vLI6+sZGHO7nKoozdXCPLfFyoDhOYwxQUXmuXPnKj8/XzU1NQNjOTk5Wrx4saqqqkb0GDfccIPKysr0xBNPjGh+d3e3XC6XfD6fUlNTgykXAOJG/TGvVmw/qot/qTv+79+apfkEEkTUSP9+B7VMc+7cOR05ckQlJSUB4yUlJTpw4MCIHqOvr09+v19paWnDzjl79qy6u7sDbgCA4fX2GVXu8gwKIpIGxip3eViywZgUVBjp6OhQb2+v0tPTA8bT09PV1tY2osd49tln9dVXX2nJkiXDzqmqqpLL5Rq4ZWZmBlMmAMSdQy1dAUszFzOSvL4eHWrpilxRwAiFdAKrw+EIuG+MGTQ2lJ07d+rJJ59UbW2tJk+ePOy89evXy+fzDdxOnToVSpkAEDfa/cMHkVDmAZEU1AmskyZNUmJi4qCjIO3t7YOOllystrZWy5cv1xtvvKHbb7/9knNTUlKUkpISTGkAENcmT3CGdR4QSUEdGUlOTlZBQYEaGhoCxhsaGlRcXDzsdjt37tQDDzyg119/XXfddVdolQIAhlWYnSa3y6nhjlE7dOGqmsLs4c/XA2wJeplm7dq1eu2117RlyxYdP35ca9asUWtrq8rLyyVdWGJZtmzZwPydO3dq2bJlevbZZ3XLLbeora1NbW1t8vl84XsVABDnEhMcqijNlaRBgaT/fkVprhITvn9JHYi0oMNIWVmZnnvuOW3cuFE33XST9u7dq7q6OmVlZUmSvF6vWltbB+a/8sorOn/+vH71q1/J7XYP3FatWhW+VwEA0MI8t2qW5ivDFbgUk+FyclkvxrSg+4zYQJ8RABi53j6jQy1davf3aPKEC0szkT4iMhZqgH0j/fsddAdWAMDYlpjgUNGMidaeny6wCBZflAcACJv+LrAX9zxp8/Voxfajqj/mtVQZxjLCCAAgLOgCi1ARRgAAYUEXWISKMAIACAu6wCJUhBEAQFjQBRahIowAAMKCLrAIFWEEABAWdIFFqAgjAICwoQssQkHTMwBAWC3Mc2tBbgYdWDFihBEAQNjZ7gKL6MIyDQAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCq+KA9ASHr7DN/KCiAsCCMAglZ/zKvKXR55fT0DY26XUxWluVqY57ZYGYBoxDINgKDUH/NqxfajAUFEktp8PVqx/ajqj3ktVQYgWhFGAIxYb59R5S6PzBA/6x+r3OVRb99QMwBgaIQRACN2qKVr0BGR7zKSvL4eHWrpilxRAKIeYQTAiLX7hw8iocwDAIkwAiAIkyc4wzoPACTCCIAgFGanye1yargLeB26cFVNYXZaJMsCEOUIIwBGLDHBoYrSXEkaFEj671eU5tJvBEBQCCMAgrIwz62apfnKcAUuxWS4nKpZmk+fkQjq7TM6eKJTf2/+TAdPdHIVE6IWTc8ABG1hnlsLcjPowGoRjecQSxzGmDEfpbu7u+VyueTz+ZSamhqWx6SVNYBo1d947uJf3v2/wThChbFipH+/4/LICJ8oAESr72s859CFxnMLcjP4gIWoEXfnjNDKGkA0o/EcYlFchRFaWQOIdjSeQyyKqzDCJwoA0Y7Gc4hFcRVG+EQBINrReA6xKK7CCJ8oAEQ7Gs8hFsVVGOETBYBYQOM5xJq4urS3/xPFiu1H5ZACTmTlEwWAaELjOcSSuGx6Rp8RAABGH03PLoFPFAAAjB1xGUakC0s2RTMm2i4DAIC4F1cnsAIAgLGHMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwKio6sPZ/fU53d7flSgAAwEj1/93+vq/Bi4ow4vf7JUmZmZmWKwEAAMHy+/1yuVzD/jwqvrW3r69PZ86c0YQJE+Rw8GV2o627u1uZmZk6depUWL4lGcFjH9jHPrCL99++cOwDY4z8fr+mTJmihIThzwyJiiMjCQkJmjZtmu0y4k5qaiq/BCxjH9jHPrCL99++y90Hlzoi0o8TWAEAgFWEEQAAYBVhBIOkpKSooqJCKSkptkuJW+wD+9gHdvH+2xfJfRAVJ7ACAIDYxZERAABgFWEEAABYRRgBAABWEUYAAIBVhJE4VV1drezsbDmdThUUFKixsXHYuW+//bYWLFigq6++WqmpqSoqKtL7778fwWpjUzD74Lv279+vpKQk3XTTTaNbYIwL9v0/e/asNmzYoKysLKWkpGjGjBnasmVLhKqNTcHugx07dmj27Nm68sor5Xa79eCDD6qzszNC1caWvXv3qrS0VFOmTJHD4dC77777vdvs2bNHBQUFcjqdmj59ul5++eXwFWQQd/72t7+ZcePGmVdffdV4PB6zatUqM378ePPpp58OOX/VqlXm6aefNocOHTIff/yxWb9+vRk3bpw5evRohCuPHcHug35ffPGFmT59uikpKTGzZ8+OTLExKJT3/+677zZz5841DQ0NpqWlxXzwwQdm//79Eaw6tgS7DxobG01CQoL585//bE6ePGkaGxvNDTfcYBYvXhzhymNDXV2d2bBhg3nrrbeMJPPOO+9ccv7JkyfNlVdeaVatWmU8Ho959dVXzbhx48ybb74ZlnoII3GosLDQlJeXB4xdf/31Zt26dSN+jNzcXFNZWRnu0uJGqPugrKzM/P73vzcVFRWEkcsQ7Pv/j3/8w7hcLtPZ2RmJ8uJCsPvgj3/8o5k+fXrA2PPPP2+mTZs2ajXGi5GEkd/+9rfm+uuvDxh7+OGHzS233BKWGlimiTPnzp3TkSNHVFJSEjBeUlKiAwcOjOgx+vr65Pf7lZaWNholxrxQ98HWrVt14sQJVVRUjHaJMS2U9/+9997TnDlz9Mwzz2jq1KmaNWuWHnvsMX3zzTeRKDnmhLIPiouLdfr0adXV1ckYo88//1xvvvmm7rrrrkiUHPcOHjw4aH/dcccdOnz4sL799tvLfvyo+KI8hE9HR4d6e3uVnp4eMJ6enq62trYRPcazzz6rr776SkuWLBmNEmNeKPvgk08+0bp169TY2KikJP7bXo5Q3v+TJ09q3759cjqdeuedd9TR0aFHHnlEXV1dnDcSglD2QXFxsXbs2KGysjL19PTo/Pnzuvvuu/XCCy9EouS419bWNuT+On/+vDo6OuR2uy/r8TkyEqccDkfAfWPMoLGh7Ny5U08++aRqa2s1efLk0SovLox0H/T29uree+9VZWWlZs2aFanyYl4w/wf6+vrkcDi0Y8cOFRYW6s4779SmTZu0bds2jo5chmD2gcfj0cqVK/XEE0/oyJEjqq+vV0tLi8rLyyNRKjT0/hpqPBR8xIozkyZNUmJi4qBPH+3t7YNS78Vqa2u1fPlyvfHGG7r99ttHs8yYFuw+8Pv9Onz4sJqamvToo49KuvDH0RijpKQk7d69W7fddltEao8FofwfcLvdmjp1asBXoefk5MgYo9OnT2vmzJmjWnOsCWUfVFVVad68eXr88cclSTfeeKPGjx+v+fPn66mnnrrsT+a4tIyMjCH3V1JSkiZOnHjZj8+RkTiTnJysgoICNTQ0BIw3NDSouLh42O127typBx54QK+//jprtJcp2H2QmpqqDz/8UM3NzQO38vJyXXfddWpubtbcuXMjVXpMCOX/wLx583TmzBl9+eWXA2Mff/yxEhISNG3atFGtNxaFsg++/vprJSQE/slKTEyU9P+f0DF6ioqKBu2v3bt3a86cORo3btzlP0FYToNFVOm/pG7z5s3G4/GY1atXm/Hjx5v//ve/xhhj1q1bZ+67776B+a+//rpJSkoyL730kvF6vQO3L774wtZLiHrB7oOLcTXN5Qn2/ff7/WbatGnm5z//ufnoo4/Mnj17zMyZM81DDz1k6yVEvWD3wdatW01SUpKprq42J06cMPv27TNz5swxhYWFtl5CVPP7/aapqck0NTUZSWbTpk2mqalp4NLqi9///kt716xZYzwej9m8eTOX9uLyvfTSSyYrK8skJyeb/Px8s2fPnoGf3X///ebWW28duH/rrbcaSYNu999/f+QLjyHB7IOLEUYuX7Dv//Hjx83tt99urrjiCjNt2jSzdu1a8/XXX0e46tgS7D54/vnnTW5urrniiiuM2+02v/jFL8zp06cjXHVs+Oc//3nJ3+tDvf//+te/zM0332ySk5PNtddea2pqasJWj8MYjm8BAAB7OGcEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABg1f8C2I7ZC9TwoakAAAAASUVORK5CYII=", + "image/png": "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Ykw0TrMjdsTHuVRalCVJgwJJ/+PSoqyI7jdCGAGAGBON0+yILUuzvapckaNMT/BnJNPjVuWKnNjYZ6SiokI+n09ut1u5ubmqq6sbtu+rr76qxYsX68///M+VmpqqgoIC/fd//3fIBQPAeBeN0+yIPUuzvTr0va9p54Pz9dS35mjng/N16Htfs3InluMwsmvXLq1du1abNm1SfX29Fi1apGXLlqm5uXnI/gcPHtTixYtVXV2t48eP66/+6q9UVFSk+vr6qy4eAMajaJxmR2yKj3OpYGa67pxznQpmplv7zLiMMY4ut543b55ycnJUWVk50DZ79mwtX75c5eXlI3qOm2++WcXFxXrsscdG1L+zs1Mej0eBQECpqalOygWAMYt9RhDtRvr97WjTs4sXL+r48ePasGFDUHthYaEOHz48oufo6+tTV1eX0tKYPgSAq7E026vFWZlR8UNnwNVwFEba29vV29urjIyMoPaMjAy1traO6Dn+/d//XX/84x911113Ddunp6dHPT09A487OzudlAkA40b/NDsQy0K6gNXlCk7dxphBbUPZuXOnfvCDH2jXrl2aPHnysP3Ky8vl8XgGjmnTpoVSJgAAiAGOwsikSZMUHx8/aBakra1t0GzJZ+3atUv333+//uM//kO33Xbb5/bduHGjAoHAwNHS0uKkTAAAEEMchZHExETl5uaqtrY2qL22tlYLFiwY9rydO3fq3nvv1c9//nPdcccdV3ydpKQkpaamBh0AAGBscvyrvevXr9fKlSuVl5engoICbd26Vc3NzSopKZF0eVbjvffe08svvyzpchBZtWqVnnrqKc2fP39gViU5OVkej2cU3woAAIhFjsNIcXGxOjo6tHnzZvn9fmVnZ6u6ulrTp0+XJPn9/qA9R5577jldunRJDz30kB566KGB9nvuuUcvvfTS1b8DAAAQ0xzvM2ID+4wAABB7Rvr9zW/TAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArEqwXQAAYOR6+4yONl1QW1e3Jqe4le9LU3ycy3ZZwFUhjABAjKhp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", "text/plain": [ "
" ] @@ -700,7 +732,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 24, "id": "1cd000bd-9b24-4c39-9cac-70a3291d0660", "metadata": {}, "outputs": [], @@ -727,7 +759,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 25, "id": "7964df3c-55af-4c25-afc5-9e07accb606a", "metadata": {}, "outputs": [ @@ -768,7 +800,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 26, "id": "809178a5-2e6b-471d-89ef-0797db47c5ad", "metadata": {}, "outputs": [ @@ -822,7 +854,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 27, "id": "52c48d19-10a2-4c48-ae81-eceea4129a60", "metadata": {}, "outputs": [ @@ -832,7 +864,7 @@ "{'ay': 3, 'a + b + 2': 7}" ] }, - "execution_count": 26, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -852,7 +884,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 28, "id": "bb35ba3e-602d-4c9c-b046-32da9401dd1c", "metadata": {}, "outputs": [ @@ -862,7 +894,7 @@ "(7, 3)" ] }, - "execution_count": 27, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -881,7 +913,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 29, "id": "2b0d2c85-9049-417b-8739-8a8432a1efbe", "metadata": {}, "outputs": [ @@ -1199,10 +1231,10 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 28, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } @@ -1223,58 +1255,10 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "id": "ae500d5e-e55b-432c-8b5f-d5892193cdf5", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5276705f13934382a5419737139590e7", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", - " warn(\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The job JUSTAJOBNAME was saved and received the ID: 9558\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "wf = Workflow(\"with_prebuilt\")\n", "\n", @@ -1301,222 +1285,10 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "id": "2114d0c3-cdad-43c7-9ffa-50c36d56d18f", "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterwith_prebuilt\n", - "\n", - "with_prebuilt: Workflow\n", - "\n", - "clusterwith_prebuiltInputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Inputs\n", - "\n", - "\n", - "clusterwith_prebuiltOutputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Outputs\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltInputsrun\n", - "\n", - "run\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltOutputsran\n", - "\n", - "ran\n", - "\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltInputsstructure__name\n", - "\n", - "structure__name\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltInputsstructure__crystalstructure\n", - "\n", - "structure__crystalstructure\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltInputsstructure__a\n", - "\n", - "structure__a\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltInputsstructure__c\n", - "\n", - "structure__c\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltInputsstructure__covera\n", - "\n", - "structure__covera\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltInputsstructure__u\n", - "\n", - "structure__u\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltInputsstructure__orthorhombic\n", - "\n", - "structure__orthorhombic\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltInputsstructure__cubic\n", - "\n", - "structure__cubic\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltInputscalc__n_ionic_steps\n", - "\n", - "calc__n_ionic_steps: int\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltInputscalc__n_print\n", - "\n", - "calc__n_print: int\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltInputscalc__temperature\n", - "\n", - "calc__temperature\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltInputscalc__pressure\n", - "\n", - "calc__pressure\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltOutputscalc__cells\n", - "\n", - "calc__cells\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltOutputscalc__displacements\n", - "\n", - "calc__displacements\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltOutputscalc__energy_pot\n", - "\n", - "calc__energy_pot\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltOutputscalc__energy_tot\n", - "\n", - "calc__energy_tot\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltOutputscalc__force_max\n", - "\n", - "calc__force_max\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltOutputscalc__forces\n", - "\n", - "calc__forces\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltOutputscalc__indices\n", - "\n", - "calc__indices\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltOutputscalc__positions\n", - "\n", - "calc__positions\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltOutputscalc__pressures\n", - "\n", - "calc__pressures\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltOutputscalc__total_displacements\n", - "\n", - "calc__total_displacements\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltOutputscalc__unwrapped_positions\n", - "\n", - "calc__unwrapped_positions\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltOutputscalc__volume\n", - "\n", - "calc__volume\n", - "\n", - "\n", - "\n", - "clusterwith_prebuiltOutputsplot__fig\n", - "\n", - "plot__fig\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "wf.draw(depth=0)" ] @@ -1533,7 +1305,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "id": "c71a8308-f8a1-4041-bea0-1c841e072a6d", "metadata": {}, "outputs": [], @@ -1543,29 +1315,10 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "id": "2b9bb21a-73cd-444e-84a9-100e202aa422", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", - " warn(\n" - ] - }, - { - "data": { - "text/plain": [ - "13" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "@Workflow.wrap_as.single_value_node(\"result\")\n", "def add_one(x):\n", @@ -1600,21 +1353,10 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "id": "3668f9a9-adca-48a4-84ea-13add965897c", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'intermediate': 102, 'plus_three': 103}" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "@Workflow.wrap_as.macro_node()\n", "def add_three_macro(macro: Macro) -> None:\n", @@ -1648,7 +1390,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "id": "9aaeeec0-5f88-4c94-a6cc-45b56d2f0111", "metadata": {}, "outputs": [], @@ -1677,7 +1419,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "id": "a832e552-b3cc-411a-a258-ef21574fc439", "metadata": {}, "outputs": [], @@ -1704,1258 +1446,20 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "id": "b764a447-236f-4cb7-952a-7cba4855087d", "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preference\n", - "\n", - "phase_preference: Workflow\n", - "\n", - "clusterphase_preferenceInputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Inputs\n", - "\n", - "\n", - "clusterphase_preferenceOutputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Outputs\n", - "\n", - "\n", - "clusterphase_preferenceelement\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "element: UserInput\n", - "\n", - "\n", - "clusterphase_preferenceelementInputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Inputs\n", - "\n", - "\n", - "clusterphase_preferenceelementOutputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Outputs\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "min_phase1: LammpsMinimize\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Inputs\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Outputs\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "min_phase2: LammpsMinimize\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Inputs\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Outputs\n", - "\n", - "\n", - "clusterphase_preferencecompare\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "compare: PerAtomEnergyDifference\n", - "\n", - "\n", - "clusterphase_preferencecompareInputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Inputs\n", - "\n", - "\n", - "clusterphase_preferencecompareOutputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Outputs\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsrun\n", - "\n", - "run\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsran\n", - "\n", - "ran\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputselement\n", - "\n", - "element\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceelementInputsuser_input\n", - "\n", - "user_input\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputselement->clusterphase_preferenceelementInputsuser_input\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsphase1\n", - "\n", - "phase1\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputscrystalstructure\n", - "\n", - "crystalstructure\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsphase1->clusterphase_preferencemin_phase1Inputscrystalstructure\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputslattice_guess1\n", - "\n", - "lattice_guess1\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputslattice_guess\n", - "\n", - "lattice_guess\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputslattice_guess1->clusterphase_preferencemin_phase1Inputslattice_guess\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__structure__c\n", - "\n", - "min_phase1__structure__c\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputsstructure__c\n", - "\n", - "structure__c\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__structure__c->clusterphase_preferencemin_phase1Inputsstructure__c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__structure__covera\n", - "\n", - "min_phase1__structure__covera\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputsstructure__covera\n", - "\n", - "structure__covera\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__structure__covera->clusterphase_preferencemin_phase1Inputsstructure__covera\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__structure__u\n", - "\n", - "min_phase1__structure__u\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputsstructure__u\n", - "\n", - "structure__u\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__structure__u->clusterphase_preferencemin_phase1Inputsstructure__u\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__structure__orthorhombic\n", - "\n", - "min_phase1__structure__orthorhombic\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputsstructure__orthorhombic\n", - "\n", - "structure__orthorhombic\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__structure__orthorhombic->clusterphase_preferencemin_phase1Inputsstructure__orthorhombic\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__structure__cubic\n", - "\n", - "min_phase1__structure__cubic\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputsstructure__cubic\n", - "\n", - "structure__cubic\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__structure__cubic->clusterphase_preferencemin_phase1Inputsstructure__cubic\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__calc__n_ionic_steps\n", - "\n", - "min_phase1__calc__n_ionic_steps: int\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputscalc__n_ionic_steps\n", - "\n", - "calc__n_ionic_steps: int\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__calc__n_ionic_steps->clusterphase_preferencemin_phase1Inputscalc__n_ionic_steps\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__calc__n_print\n", - "\n", - "min_phase1__calc__n_print: int\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputscalc__n_print\n", - "\n", - "calc__n_print: int\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__calc__n_print->clusterphase_preferencemin_phase1Inputscalc__n_print\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__calc__pressure\n", - "\n", - "min_phase1__calc__pressure\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputscalc__pressure\n", - "\n", - "calc__pressure\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase1__calc__pressure->clusterphase_preferencemin_phase1Inputscalc__pressure\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsphase2\n", - "\n", - "phase2\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputscrystalstructure\n", - "\n", - "crystalstructure\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsphase2->clusterphase_preferencemin_phase2Inputscrystalstructure\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputslattice_guess2\n", - "\n", - "lattice_guess2\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputslattice_guess\n", - "\n", - "lattice_guess\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputslattice_guess2->clusterphase_preferencemin_phase2Inputslattice_guess\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__structure__c\n", - "\n", - "min_phase2__structure__c\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputsstructure__c\n", - "\n", - "structure__c\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__structure__c->clusterphase_preferencemin_phase2Inputsstructure__c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__structure__covera\n", - "\n", - "min_phase2__structure__covera\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputsstructure__covera\n", - "\n", - "structure__covera\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__structure__covera->clusterphase_preferencemin_phase2Inputsstructure__covera\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__structure__u\n", - "\n", - "min_phase2__structure__u\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputsstructure__u\n", - "\n", - "structure__u\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__structure__u->clusterphase_preferencemin_phase2Inputsstructure__u\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__structure__orthorhombic\n", - "\n", - "min_phase2__structure__orthorhombic\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputsstructure__orthorhombic\n", - "\n", - "structure__orthorhombic\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__structure__orthorhombic->clusterphase_preferencemin_phase2Inputsstructure__orthorhombic\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__structure__cubic\n", - "\n", - "min_phase2__structure__cubic\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputsstructure__cubic\n", - "\n", - "structure__cubic\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__structure__cubic->clusterphase_preferencemin_phase2Inputsstructure__cubic\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__calc__n_ionic_steps\n", - "\n", - "min_phase2__calc__n_ionic_steps: int\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputscalc__n_ionic_steps\n", - "\n", - "calc__n_ionic_steps: int\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__calc__n_ionic_steps->clusterphase_preferencemin_phase2Inputscalc__n_ionic_steps\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__calc__n_print\n", - "\n", - "min_phase2__calc__n_print: int\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputscalc__n_print\n", - "\n", - "calc__n_print: int\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__calc__n_print->clusterphase_preferencemin_phase2Inputscalc__n_print\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__calc__pressure\n", - "\n", - "min_phase2__calc__pressure\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputscalc__pressure\n", - "\n", - "calc__pressure\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceInputsmin_phase2__calc__pressure->clusterphase_preferencemin_phase2Inputscalc__pressure\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase1__calc__cells\n", - "\n", - "min_phase1__calc__cells\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase1__calc__displacements\n", - "\n", - "min_phase1__calc__displacements\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase1__calc__energy_tot\n", - "\n", - "min_phase1__calc__energy_tot\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase1__calc__force_max\n", - "\n", - "min_phase1__calc__force_max\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase1__calc__forces\n", - "\n", - "min_phase1__calc__forces\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase1__calc__indices\n", - "\n", - "min_phase1__calc__indices\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase1__calc__positions\n", - "\n", - "min_phase1__calc__positions\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase1__calc__pressures\n", - "\n", - "min_phase1__calc__pressures\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase1__calc__steps\n", - "\n", - "min_phase1__calc__steps\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase1__calc__total_displacements\n", - "\n", - "min_phase1__calc__total_displacements\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase1__calc__unwrapped_positions\n", - "\n", - "min_phase1__calc__unwrapped_positions\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase1__calc__volume\n", - "\n", - "min_phase1__calc__volume\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase2__calc__cells\n", - "\n", - "min_phase2__calc__cells\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase2__calc__displacements\n", - "\n", - "min_phase2__calc__displacements\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase2__calc__energy_tot\n", - "\n", - "min_phase2__calc__energy_tot\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase2__calc__force_max\n", - "\n", - "min_phase2__calc__force_max\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase2__calc__forces\n", - "\n", - "min_phase2__calc__forces\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase2__calc__indices\n", - "\n", - "min_phase2__calc__indices\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase2__calc__positions\n", - "\n", - "min_phase2__calc__positions\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase2__calc__pressures\n", - "\n", - "min_phase2__calc__pressures\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase2__calc__steps\n", - "\n", - "min_phase2__calc__steps\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase2__calc__total_displacements\n", - "\n", - "min_phase2__calc__total_displacements\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase2__calc__unwrapped_positions\n", - "\n", - "min_phase2__calc__unwrapped_positions\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputsmin_phase2__calc__volume\n", - "\n", - "min_phase2__calc__volume\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceOutputscompare__de\n", - "\n", - "compare__de\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceelementInputsrun\n", - "\n", - "run\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceelementOutputsran\n", - "\n", - "ran\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceelementOutputsuser_input\n", - "\n", - "user_input\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputselement\n", - "\n", - "element\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceelementOutputsuser_input->clusterphase_preferencemin_phase1Inputselement\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputselement\n", - "\n", - "element\n", - "\n", - "\n", - "\n", - "clusterphase_preferenceelementOutputsuser_input->clusterphase_preferencemin_phase2Inputselement\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputsrun\n", - "\n", - "run\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputsran\n", - "\n", - "ran\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputsstructure\n", - "\n", - "structure\n", - "\n", - "\n", - "\n", - "clusterphase_preferencecompareInputsstructure1\n", - "\n", - "structure1\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputsstructure->clusterphase_preferencecompareInputsstructure1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__cells\n", - "\n", - "calc__cells\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__cells->clusterphase_preferenceOutputsmin_phase1__calc__cells\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__displacements\n", - "\n", - "calc__displacements\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__displacements->clusterphase_preferenceOutputsmin_phase1__calc__displacements\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputsenergy\n", - "\n", - "energy\n", - "\n", - "\n", - "\n", - "clusterphase_preferencecompareInputsenergy1\n", - "\n", - "energy1\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputsenergy->clusterphase_preferencecompareInputsenergy1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__energy_tot\n", - "\n", - "calc__energy_tot\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__energy_tot->clusterphase_preferenceOutputsmin_phase1__calc__energy_tot\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__force_max\n", - "\n", - "calc__force_max\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__force_max->clusterphase_preferenceOutputsmin_phase1__calc__force_max\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__forces\n", - "\n", - "calc__forces\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__forces->clusterphase_preferenceOutputsmin_phase1__calc__forces\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__indices\n", - "\n", - "calc__indices\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__indices->clusterphase_preferenceOutputsmin_phase1__calc__indices\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__positions\n", - "\n", - "calc__positions\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__positions->clusterphase_preferenceOutputsmin_phase1__calc__positions\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__pressures\n", - "\n", - "calc__pressures\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__pressures->clusterphase_preferenceOutputsmin_phase1__calc__pressures\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__steps\n", - "\n", - "calc__steps\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__steps->clusterphase_preferenceOutputsmin_phase1__calc__steps\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__total_displacements\n", - "\n", - "calc__total_displacements\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__total_displacements->clusterphase_preferenceOutputsmin_phase1__calc__total_displacements\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__unwrapped_positions\n", - "\n", - "calc__unwrapped_positions\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__unwrapped_positions->clusterphase_preferenceOutputsmin_phase1__calc__unwrapped_positions\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__volume\n", - "\n", - "calc__volume\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputscalc__volume->clusterphase_preferenceOutputsmin_phase1__calc__volume\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Inputsrun\n", - "\n", - "run\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputsran\n", - "\n", - "ran\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputsstructure\n", - "\n", - "structure\n", - "\n", - "\n", - "\n", - "clusterphase_preferencecompareInputsstructure2\n", - "\n", - "structure2\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputsstructure->clusterphase_preferencecompareInputsstructure2\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__cells\n", - "\n", - "calc__cells\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__cells->clusterphase_preferenceOutputsmin_phase2__calc__cells\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__displacements\n", - "\n", - "calc__displacements\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__displacements->clusterphase_preferenceOutputsmin_phase2__calc__displacements\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputsenergy\n", - "\n", - "energy\n", - "\n", - "\n", - "\n", - "clusterphase_preferencecompareInputsenergy2\n", - "\n", - "energy2\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputsenergy->clusterphase_preferencecompareInputsenergy2\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__energy_tot\n", - "\n", - "calc__energy_tot\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__energy_tot->clusterphase_preferenceOutputsmin_phase2__calc__energy_tot\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__force_max\n", - "\n", - "calc__force_max\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__force_max->clusterphase_preferenceOutputsmin_phase2__calc__force_max\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__forces\n", - "\n", - "calc__forces\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__forces->clusterphase_preferenceOutputsmin_phase2__calc__forces\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__indices\n", - "\n", - "calc__indices\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__indices->clusterphase_preferenceOutputsmin_phase2__calc__indices\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__positions\n", - "\n", - "calc__positions\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__positions->clusterphase_preferenceOutputsmin_phase2__calc__positions\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__pressures\n", - "\n", - "calc__pressures\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__pressures->clusterphase_preferenceOutputsmin_phase2__calc__pressures\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__steps\n", - "\n", - "calc__steps\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__steps->clusterphase_preferenceOutputsmin_phase2__calc__steps\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__total_displacements\n", - "\n", - "calc__total_displacements\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__total_displacements->clusterphase_preferenceOutputsmin_phase2__calc__total_displacements\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__unwrapped_positions\n", - "\n", - "calc__unwrapped_positions\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__unwrapped_positions->clusterphase_preferenceOutputsmin_phase2__calc__unwrapped_positions\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__volume\n", - "\n", - "calc__volume\n", - "\n", - "\n", - "\n", - "clusterphase_preferencemin_phase2Outputscalc__volume->clusterphase_preferenceOutputsmin_phase2__calc__volume\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencecompareInputsrun\n", - "\n", - "run\n", - "\n", - "\n", - "\n", - "clusterphase_preferencecompareOutputsran\n", - "\n", - "ran\n", - "\n", - "\n", - "\n", - "\n", - "clusterphase_preferencecompareOutputsde\n", - "\n", - "de\n", - "\n", - "\n", - "\n", - "clusterphase_preferencecompareOutputsde->clusterphase_preferenceOutputscompare__de\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "wf.draw()" ] }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "id": "b51bef25-86c5-4d57-80c1-ab733e703caf", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The job JUSTAJOBNAME was saved and received the ID: 9558\n", - "The job JUSTAJOBNAME was saved and received the ID: 9558\n", - "Al: E(hcp) - E(fcc) = 1.17 eV/atom\n" - ] - } - ], + "outputs": [], "source": [ "out = wf(element=\"Al\", phase1=\"fcc\", phase2=\"hcp\", lattice_guess1=4, lattice_guess2=4)\n", "print(f\"{wf.inputs.element.value}: E({wf.inputs.phase2.value}) - E({wf.inputs.phase1.value}) = {out.compare__de:.2f} eV/atom\")" @@ -2963,28 +1467,10 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": null, "id": "091e2386-0081-436c-a736-23d019bd9b91", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", - " warn(\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The job JUSTAJOBNAME was saved and received the ID: 9558\n", - "The job JUSTAJOBNAME was saved and received the ID: 9558\n", - "Mg: E(hcp) - E(fcc) = -4.54 eV/atom\n" - ] - } - ], + "outputs": [], "source": [ "out = wf(element=\"Mg\", phase1=\"fcc\", phase2=\"hcp\", lattice_guess1=3, lattice_guess2=3)\n", "print(f\"{wf.inputs.element.value}: E({wf.inputs.phase2.value}) - E({wf.inputs.phase1.value}) = {out.compare__de:.2f} eV/atom\")" @@ -3004,23 +1490,10 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": null, "id": "4cdffdca-48d3-4486-9045-48102c7e5f31", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel job was not connected to job, andthus could not disconnect from it.\n", - " warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel energy_pot was not connected to energy1, andthus could not disconnect from it.\n", - " warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", - " warn(\n" - ] - } - ], + "outputs": [], "source": [ "wf.min_phase1.calc = Macro.create.atomistics.CalcStatic" ] @@ -3037,20 +1510,10 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": null, "id": "ed4a3a22-fc3a-44c9-9d4f-c65bc1288889", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The job JUSTAJOBNAME was saved and received the ID: 9558\n", - "The job JUSTAJOBNAME was saved and received the ID: 9558\n", - "Al: E(hcp) - E(fcc) = -5.57 eV/atom\n" - ] - } - ], + "outputs": [], "source": [ "# Bad guess\n", "out = wf(element=\"Al\", phase1=\"fcc\", phase2=\"hcp\", lattice_guess1=3, lattice_guess2=3)\n", @@ -3059,28 +1522,10 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": null, "id": "5a985cbf-c308-4369-9223-b8a37edb8ab1", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", - " warn(\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The job JUSTAJOBNAME was saved and received the ID: 9558\n", - "The job JUSTAJOBNAME was saved and received the ID: 9558\n", - "Al: E(hcp) - E(fcc) = 0.03 eV/atom\n" - ] - } - ], + "outputs": [], "source": [ "# Good guess\n", "out = wf(element=\"Al\", phase1=\"fcc\", phase2=\"hcp\", lattice_guess1=4.05, lattice_guess2=3)\n", @@ -3149,25 +1594,10 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": null, "id": "0b373764-b389-4c24-8086-f3d33a4f7fd7", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[14.829749999999995,\n", - " 15.407468749999998,\n", - " 15.999999999999998,\n", - " 16.60753125,\n", - " 17.230249999999995]" - ] - }, - "execution_count": 42, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "n = 5\n", "\n", @@ -3200,21 +1630,10 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": null, "id": "0dd04b4c-e3e7-4072-ad34-58f2c1e4f596", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to true, andthus could not disconnect from it.\n", - " warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", - " warn(\n" - ] - } - ], + "outputs": [], "source": [ "@Workflow.wrap_as.single_value_node()\n", "def add(a, b):\n", @@ -3259,23 +1678,10 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": null, "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" - ] - } - ], + "outputs": [], "source": [ "response = wf(a=1, b=2)\n", "print(\"Finally\", response)" @@ -3283,24 +1689,10 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": null, "id": "2e87f858-b327-4f6b-9237-c8a557f29aeb", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.710 > 0.2\n", - "0.757 > 0.2\n", - "0.651 > 0.2\n", - "0.821 > 0.2\n", - "0.677 > 0.2\n", - "0.189 <= 0.2\n", - "Finally 0.189\n" - ] - } - ], + "outputs": [], "source": [ "@Workflow.wrap_as.single_value_node(\"random\")\n", "def random(length: int | None = None):\n", diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index fa20e52a7..ca43c0323 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -233,8 +233,8 @@ class Function(Node): Because we provided a good initial value for `x`, we get our result right away. Using the decorator is the recommended way to create new node classes, but this - magic is just equivalent to these two more verbose ways of defining a new class. - The first is to override the `__init__` method directly: + magic is just equivalent to creating a child class with the `node_function` + already defined as a `staticmethod`: >>> from typing import Literal, Optional >>> >>> class AlphabetModThree(Function): @@ -254,24 +254,6 @@ class Function(Node): ... letter = ["a", "b", "c"][i % 3] ... return letter - The second effectively does the same thing, but leverages python's - `functools.partialmethod` to do so much more succinctly. - In this example, note that the function is declared _before_ `__init__` is set, - so that it is available in the correct scope (above, we could place it - afterwards because we were accessing it through self). - >>> from functools import partialmethod - >>> - >>> class Adder(Function): - ... @staticmethod - ... def adder(x: int = 0, y: int = 0) -> int: - ... sum = x + y - ... return sum - ... - ... __init__ = partialmethod( - ... Function.__init__, - ... adder, - ... ) - Finally, let's put it all together by using both of these nodes at once. Instead of setting input to a particular data value, we'll set it to be another node's output channel, thus forming a connection. @@ -322,14 +304,29 @@ def __init__( output_labels: Optional[str | list[str] | tuple[str]] = None, **kwargs, ): + if not callable(node_function): + # Children of `Function` may explicitly provide a `node_function` static + # method so the node has fixed behaviour. + # In this case, the `__init__` signature should be changed so that the + # `node_function` argument is just always `None` or some other non-callable. + # If a callable `node_function` is not received, you'd better have it as an + # attribute already! + if not hasattr(self, "node_function"): + raise AttributeError( + f"If `None` is provided as a `node_function`, a `node_function` " + f"property must be defined instead, e.g. when making child classes" + f"of `Function` with specific behaviour" + ) + else: + # If a callable node function is received, use it + self.node_function = node_function + super().__init__( - label=label if label is not None else node_function.__name__, + label=label if label is not None else self.node_function.__name__, parent=parent, # **kwargs, ) - self.node_function = node_function - self._inputs = None self._outputs = None self._output_labels = self._get_output_labels(output_labels) @@ -639,9 +636,10 @@ def as_node(node_function: callable): { "__init__": partialmethod( Function.__init__, - node_function, + None, output_labels=output_labels, - ) + ), + "node_function": staticmethod(node_function), }, ) @@ -664,9 +662,10 @@ def as_single_value_node(node_function: callable): { "__init__": partialmethod( SingleValue.__init__, - node_function, + None, output_labels=output_labels, - ) + ), + "node_function": staticmethod(node_function), }, ) diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 8c426e0fd..66ee5b0bd 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -147,6 +147,23 @@ def test_label_choices(self): switch = Function(multiple_branches, output_labels="bool") self.assertListEqual(switch.outputs.labels, ["bool"]) + def test_availability_of_node_function(self): + @function_node() + def linear(x): + return x + + @function_node() + def bilinear(x, y): + xy = linear.node_function(x) * linear.node_function(y) + return xy + + self.assertEqual( + bilinear(2, 3).run(), + 2 * 3, + msg="Children of `Function` should have their `node_function` exposed for " + "use at the class level" + ) + def test_signals(self): @function_node() def linear(x): From c3d24a043406cbddd8b50eea00d03eec493cf830 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 16 Oct 2023 10:10:22 -0700 Subject: [PATCH 37/97] Execute demo notebook --- notebooks/workflow_example.ipynb | 1700 +++++++++++++++++++++++++++++- 1 file changed, 1669 insertions(+), 31 deletions(-) diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index 77fdca1df..02ba8d4ff 100644 --- a/notebooks/workflow_example.ipynb +++ b/notebooks/workflow_example.ipynb @@ -1255,10 +1255,58 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "id": "ae500d5e-e55b-432c-8b5f-d5892193cdf5", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "67e245355ef74e23999a2b46f5f4cbaa", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The job JUSTAJOBNAME was saved and received the ID: 9558\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "wf = Workflow(\"with_prebuilt\")\n", "\n", @@ -1285,10 +1333,222 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "id": "2114d0c3-cdad-43c7-9ffa-50c36d56d18f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterwith_prebuilt\n", + "\n", + "with_prebuilt: Workflow\n", + "\n", + "clusterwith_prebuiltInputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", + "\n", + "clusterwith_prebuiltOutputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Outputs\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltInputsrun\n", + "\n", + "run\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltOutputsran\n", + "\n", + "ran\n", + "\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltInputsstructure__name\n", + "\n", + "structure__name\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltInputsstructure__crystalstructure\n", + "\n", + "structure__crystalstructure\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltInputsstructure__a\n", + "\n", + "structure__a\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltInputsstructure__c\n", + "\n", + "structure__c\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltInputsstructure__covera\n", + "\n", + "structure__covera\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltInputsstructure__u\n", + "\n", + "structure__u\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltInputsstructure__orthorhombic\n", + "\n", + "structure__orthorhombic\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltInputsstructure__cubic\n", + "\n", + "structure__cubic\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltInputscalc__n_ionic_steps\n", + "\n", + "calc__n_ionic_steps: int\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltInputscalc__n_print\n", + "\n", + "calc__n_print: int\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltInputscalc__temperature\n", + "\n", + "calc__temperature\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltInputscalc__pressure\n", + "\n", + "calc__pressure\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltOutputscalc__cells\n", + "\n", + "calc__cells\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltOutputscalc__displacements\n", + "\n", + "calc__displacements\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltOutputscalc__energy_pot\n", + "\n", + "calc__energy_pot\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltOutputscalc__energy_tot\n", + "\n", + "calc__energy_tot\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltOutputscalc__force_max\n", + "\n", + "calc__force_max\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltOutputscalc__forces\n", + "\n", + "calc__forces\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltOutputscalc__indices\n", + "\n", + "calc__indices\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltOutputscalc__positions\n", + "\n", + "calc__positions\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltOutputscalc__pressures\n", + "\n", + "calc__pressures\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltOutputscalc__total_displacements\n", + "\n", + "calc__total_displacements\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltOutputscalc__unwrapped_positions\n", + "\n", + "calc__unwrapped_positions\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltOutputscalc__volume\n", + "\n", + "calc__volume\n", + "\n", + "\n", + "\n", + "clusterwith_prebuiltOutputsplot__fig\n", + "\n", + "plot__fig\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "wf.draw(depth=0)" ] @@ -1305,7 +1565,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 32, "id": "c71a8308-f8a1-4041-bea0-1c841e072a6d", "metadata": {}, "outputs": [], @@ -1315,10 +1575,29 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "id": "2b9bb21a-73cd-444e-84a9-100e202aa422", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n" + ] + }, + { + "data": { + "text/plain": [ + "13" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "@Workflow.wrap_as.single_value_node(\"result\")\n", "def add_one(x):\n", @@ -1353,10 +1632,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "id": "3668f9a9-adca-48a4-84ea-13add965897c", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{'intermediate': 102, 'plus_three': 103}" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "@Workflow.wrap_as.macro_node()\n", "def add_three_macro(macro: Macro) -> None:\n", @@ -1390,7 +1680,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 35, "id": "9aaeeec0-5f88-4c94-a6cc-45b56d2f0111", "metadata": {}, "outputs": [], @@ -1419,7 +1709,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 36, "id": "a832e552-b3cc-411a-a258-ef21574fc439", "metadata": {}, "outputs": [], @@ -1446,20 +1736,1258 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 37, "id": "b764a447-236f-4cb7-952a-7cba4855087d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preference\n", + "\n", + "phase_preference: Workflow\n", + "\n", + "clusterphase_preferenceOutputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Outputs\n", + "\n", + "\n", + "clusterphase_preferenceelement\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "element: UserInput\n", + "\n", + "\n", + "clusterphase_preferenceelementInputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", + "\n", + "clusterphase_preferenceelementOutputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Outputs\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "min_phase2: LammpsMinimize\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Outputs\n", + "\n", + "\n", + "clusterphase_preferencecompare\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "compare: PerAtomEnergyDifference\n", + "\n", + "\n", + "clusterphase_preferencecompareOutputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Outputs\n", + "\n", + "\n", + "clusterphase_preferencecompareInputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", + "\n", + "clusterphase_preferenceInputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "min_phase1: LammpsMinimize\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Outputs\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsrun\n", + "\n", + "run\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsran\n", + "\n", + "ran\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputselement\n", + "\n", + "element\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceelementInputsuser_input\n", + "\n", + "user_input\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputselement->clusterphase_preferenceelementInputsuser_input\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsphase1\n", + "\n", + "phase1\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputscrystalstructure\n", + "\n", + "crystalstructure\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsphase1->clusterphase_preferencemin_phase1Inputscrystalstructure\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputslattice_guess1\n", + "\n", + "lattice_guess1\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputslattice_guess\n", + "\n", + "lattice_guess\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputslattice_guess1->clusterphase_preferencemin_phase1Inputslattice_guess\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__structure__c\n", + "\n", + "min_phase1__structure__c\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputsstructure__c\n", + "\n", + "structure__c\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__structure__c->clusterphase_preferencemin_phase1Inputsstructure__c\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__structure__covera\n", + "\n", + "min_phase1__structure__covera\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputsstructure__covera\n", + "\n", + "structure__covera\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__structure__covera->clusterphase_preferencemin_phase1Inputsstructure__covera\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__structure__u\n", + "\n", + "min_phase1__structure__u\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputsstructure__u\n", + "\n", + "structure__u\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__structure__u->clusterphase_preferencemin_phase1Inputsstructure__u\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__structure__orthorhombic\n", + "\n", + "min_phase1__structure__orthorhombic\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputsstructure__orthorhombic\n", + "\n", + "structure__orthorhombic\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__structure__orthorhombic->clusterphase_preferencemin_phase1Inputsstructure__orthorhombic\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__structure__cubic\n", + "\n", + "min_phase1__structure__cubic\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputsstructure__cubic\n", + "\n", + "structure__cubic\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__structure__cubic->clusterphase_preferencemin_phase1Inputsstructure__cubic\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__calc__n_ionic_steps\n", + "\n", + "min_phase1__calc__n_ionic_steps: int\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputscalc__n_ionic_steps\n", + "\n", + "calc__n_ionic_steps: int\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__calc__n_ionic_steps->clusterphase_preferencemin_phase1Inputscalc__n_ionic_steps\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__calc__n_print\n", + "\n", + "min_phase1__calc__n_print: int\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputscalc__n_print\n", + "\n", + "calc__n_print: int\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__calc__n_print->clusterphase_preferencemin_phase1Inputscalc__n_print\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__calc__pressure\n", + "\n", + "min_phase1__calc__pressure\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputscalc__pressure\n", + "\n", + "calc__pressure\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase1__calc__pressure->clusterphase_preferencemin_phase1Inputscalc__pressure\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsphase2\n", + "\n", + "phase2\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputscrystalstructure\n", + "\n", + "crystalstructure\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsphase2->clusterphase_preferencemin_phase2Inputscrystalstructure\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputslattice_guess2\n", + "\n", + "lattice_guess2\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputslattice_guess\n", + "\n", + "lattice_guess\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputslattice_guess2->clusterphase_preferencemin_phase2Inputslattice_guess\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__c\n", + "\n", + "min_phase2__structure__c\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputsstructure__c\n", + "\n", + "structure__c\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__c->clusterphase_preferencemin_phase2Inputsstructure__c\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__covera\n", + "\n", + "min_phase2__structure__covera\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputsstructure__covera\n", + "\n", + "structure__covera\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__covera->clusterphase_preferencemin_phase2Inputsstructure__covera\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__u\n", + "\n", + "min_phase2__structure__u\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputsstructure__u\n", + "\n", + "structure__u\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__u->clusterphase_preferencemin_phase2Inputsstructure__u\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__orthorhombic\n", + "\n", + "min_phase2__structure__orthorhombic\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputsstructure__orthorhombic\n", + "\n", + "structure__orthorhombic\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__orthorhombic->clusterphase_preferencemin_phase2Inputsstructure__orthorhombic\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__cubic\n", + "\n", + "min_phase2__structure__cubic\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputsstructure__cubic\n", + "\n", + "structure__cubic\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__structure__cubic->clusterphase_preferencemin_phase2Inputsstructure__cubic\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__calc__n_ionic_steps\n", + "\n", + "min_phase2__calc__n_ionic_steps: int\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputscalc__n_ionic_steps\n", + "\n", + "calc__n_ionic_steps: int\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__calc__n_ionic_steps->clusterphase_preferencemin_phase2Inputscalc__n_ionic_steps\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__calc__n_print\n", + "\n", + "min_phase2__calc__n_print: int\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputscalc__n_print\n", + "\n", + "calc__n_print: int\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__calc__n_print->clusterphase_preferencemin_phase2Inputscalc__n_print\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__calc__pressure\n", + "\n", + "min_phase2__calc__pressure\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputscalc__pressure\n", + "\n", + "calc__pressure\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceInputsmin_phase2__calc__pressure->clusterphase_preferencemin_phase2Inputscalc__pressure\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__cells\n", + "\n", + "min_phase1__calc__cells\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__displacements\n", + "\n", + "min_phase1__calc__displacements\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__energy_tot\n", + "\n", + "min_phase1__calc__energy_tot\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__force_max\n", + "\n", + "min_phase1__calc__force_max\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__forces\n", + "\n", + "min_phase1__calc__forces\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__indices\n", + "\n", + "min_phase1__calc__indices\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__positions\n", + "\n", + "min_phase1__calc__positions\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__pressures\n", + "\n", + "min_phase1__calc__pressures\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__steps\n", + "\n", + "min_phase1__calc__steps\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__total_displacements\n", + "\n", + "min_phase1__calc__total_displacements\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__unwrapped_positions\n", + "\n", + "min_phase1__calc__unwrapped_positions\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase1__calc__volume\n", + "\n", + "min_phase1__calc__volume\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__cells\n", + "\n", + "min_phase2__calc__cells\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__displacements\n", + "\n", + "min_phase2__calc__displacements\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__energy_tot\n", + "\n", + "min_phase2__calc__energy_tot\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__force_max\n", + "\n", + "min_phase2__calc__force_max\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__forces\n", + "\n", + "min_phase2__calc__forces\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__indices\n", + "\n", + "min_phase2__calc__indices\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__positions\n", + "\n", + "min_phase2__calc__positions\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__pressures\n", + "\n", + "min_phase2__calc__pressures\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__steps\n", + "\n", + "min_phase2__calc__steps\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__total_displacements\n", + "\n", + "min_phase2__calc__total_displacements\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__unwrapped_positions\n", + "\n", + "min_phase2__calc__unwrapped_positions\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputsmin_phase2__calc__volume\n", + "\n", + "min_phase2__calc__volume\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceOutputscompare__de\n", + "\n", + "compare__de\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceelementInputsrun\n", + "\n", + "run\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceelementOutputsran\n", + "\n", + "ran\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceelementOutputsuser_input\n", + "\n", + "user_input\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputselement\n", + "\n", + "element\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceelementOutputsuser_input->clusterphase_preferencemin_phase1Inputselement\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputselement\n", + "\n", + "element\n", + "\n", + "\n", + "\n", + "clusterphase_preferenceelementOutputsuser_input->clusterphase_preferencemin_phase2Inputselement\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputsrun\n", + "\n", + "run\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputsran\n", + "\n", + "ran\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputsstructure\n", + "\n", + "structure\n", + "\n", + "\n", + "\n", + "clusterphase_preferencecompareInputsstructure1\n", + "\n", + "structure1\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputsstructure->clusterphase_preferencecompareInputsstructure1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__cells\n", + "\n", + "calc__cells\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__cells->clusterphase_preferenceOutputsmin_phase1__calc__cells\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__displacements\n", + "\n", + "calc__displacements\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__displacements->clusterphase_preferenceOutputsmin_phase1__calc__displacements\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputsenergy\n", + "\n", + "energy\n", + "\n", + "\n", + "\n", + "clusterphase_preferencecompareInputsenergy1\n", + "\n", + "energy1\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputsenergy->clusterphase_preferencecompareInputsenergy1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__energy_tot\n", + "\n", + "calc__energy_tot\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__energy_tot->clusterphase_preferenceOutputsmin_phase1__calc__energy_tot\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__force_max\n", + "\n", + "calc__force_max\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__force_max->clusterphase_preferenceOutputsmin_phase1__calc__force_max\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__forces\n", + "\n", + "calc__forces\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__forces->clusterphase_preferenceOutputsmin_phase1__calc__forces\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__indices\n", + "\n", + "calc__indices\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__indices->clusterphase_preferenceOutputsmin_phase1__calc__indices\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__positions\n", + "\n", + "calc__positions\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__positions->clusterphase_preferenceOutputsmin_phase1__calc__positions\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__pressures\n", + "\n", + "calc__pressures\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__pressures->clusterphase_preferenceOutputsmin_phase1__calc__pressures\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__steps\n", + "\n", + "calc__steps\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__steps->clusterphase_preferenceOutputsmin_phase1__calc__steps\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__total_displacements\n", + "\n", + "calc__total_displacements\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__total_displacements->clusterphase_preferenceOutputsmin_phase1__calc__total_displacements\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__unwrapped_positions\n", + "\n", + "calc__unwrapped_positions\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__unwrapped_positions->clusterphase_preferenceOutputsmin_phase1__calc__unwrapped_positions\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__volume\n", + "\n", + "calc__volume\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputscalc__volume->clusterphase_preferenceOutputsmin_phase1__calc__volume\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Inputsrun\n", + "\n", + "run\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputsran\n", + "\n", + "ran\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputsstructure\n", + "\n", + "structure\n", + "\n", + "\n", + "\n", + "clusterphase_preferencecompareInputsstructure2\n", + "\n", + "structure2\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputsstructure->clusterphase_preferencecompareInputsstructure2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__cells\n", + "\n", + "calc__cells\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__cells->clusterphase_preferenceOutputsmin_phase2__calc__cells\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__displacements\n", + "\n", + "calc__displacements\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__displacements->clusterphase_preferenceOutputsmin_phase2__calc__displacements\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputsenergy\n", + "\n", + "energy\n", + "\n", + "\n", + "\n", + "clusterphase_preferencecompareInputsenergy2\n", + "\n", + "energy2\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputsenergy->clusterphase_preferencecompareInputsenergy2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__energy_tot\n", + "\n", + "calc__energy_tot\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__energy_tot->clusterphase_preferenceOutputsmin_phase2__calc__energy_tot\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__force_max\n", + "\n", + "calc__force_max\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__force_max->clusterphase_preferenceOutputsmin_phase2__calc__force_max\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__forces\n", + "\n", + "calc__forces\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__forces->clusterphase_preferenceOutputsmin_phase2__calc__forces\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__indices\n", + "\n", + "calc__indices\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__indices->clusterphase_preferenceOutputsmin_phase2__calc__indices\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__positions\n", + "\n", + "calc__positions\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__positions->clusterphase_preferenceOutputsmin_phase2__calc__positions\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__pressures\n", + "\n", + "calc__pressures\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__pressures->clusterphase_preferenceOutputsmin_phase2__calc__pressures\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__steps\n", + "\n", + "calc__steps\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__steps->clusterphase_preferenceOutputsmin_phase2__calc__steps\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__total_displacements\n", + "\n", + "calc__total_displacements\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__total_displacements->clusterphase_preferenceOutputsmin_phase2__calc__total_displacements\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__unwrapped_positions\n", + "\n", + "calc__unwrapped_positions\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__unwrapped_positions->clusterphase_preferenceOutputsmin_phase2__calc__unwrapped_positions\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__volume\n", + "\n", + "calc__volume\n", + "\n", + "\n", + "\n", + "clusterphase_preferencemin_phase2Outputscalc__volume->clusterphase_preferenceOutputsmin_phase2__calc__volume\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencecompareInputsrun\n", + "\n", + "run\n", + "\n", + "\n", + "\n", + "clusterphase_preferencecompareOutputsran\n", + "\n", + "ran\n", + "\n", + "\n", + "\n", + "\n", + "clusterphase_preferencecompareOutputsde\n", + "\n", + "de\n", + "\n", + "\n", + "\n", + "clusterphase_preferencecompareOutputsde->clusterphase_preferenceOutputscompare__de\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "wf.draw()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 38, "id": "b51bef25-86c5-4d57-80c1-ab733e703caf", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The job JUSTAJOBNAME was saved and received the ID: 9558\n", + "The job JUSTAJOBNAME was saved and received the ID: 9558\n", + "Al: E(hcp) - E(fcc) = 1.17 eV/atom\n" + ] + } + ], "source": [ "out = wf(element=\"Al\", phase1=\"fcc\", phase2=\"hcp\", lattice_guess1=4, lattice_guess2=4)\n", "print(f\"{wf.inputs.element.value}: E({wf.inputs.phase2.value}) - E({wf.inputs.phase1.value}) = {out.compare__de:.2f} eV/atom\")" @@ -1467,10 +2995,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 39, "id": "091e2386-0081-436c-a736-23d019bd9b91", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The job JUSTAJOBNAME was saved and received the ID: 9558\n", + "The job JUSTAJOBNAME was saved and received the ID: 9558\n", + "Mg: E(hcp) - E(fcc) = -4.54 eV/atom\n" + ] + } + ], "source": [ "out = wf(element=\"Mg\", phase1=\"fcc\", phase2=\"hcp\", lattice_guess1=3, lattice_guess2=3)\n", "print(f\"{wf.inputs.element.value}: E({wf.inputs.phase2.value}) - E({wf.inputs.phase1.value}) = {out.compare__de:.2f} eV/atom\")" @@ -1490,10 +3036,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 40, "id": "4cdffdca-48d3-4486-9045-48102c7e5f31", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel job was not connected to job, andthus could not disconnect from it.\n", + " warn(\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel energy_pot was not connected to energy1, andthus could not disconnect from it.\n", + " warn(\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n" + ] + } + ], "source": [ "wf.min_phase1.calc = Macro.create.atomistics.CalcStatic" ] @@ -1510,10 +3069,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 41, "id": "ed4a3a22-fc3a-44c9-9d4f-c65bc1288889", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The job JUSTAJOBNAME was saved and received the ID: 9558\n", + "The job JUSTAJOBNAME was saved and received the ID: 9558\n", + "Al: E(hcp) - E(fcc) = -5.57 eV/atom\n" + ] + } + ], "source": [ "# Bad guess\n", "out = wf(element=\"Al\", phase1=\"fcc\", phase2=\"hcp\", lattice_guess1=3, lattice_guess2=3)\n", @@ -1522,10 +3091,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 42, "id": "5a985cbf-c308-4369-9223-b8a37edb8ab1", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The job JUSTAJOBNAME was saved and received the ID: 9558\n", + "The job JUSTAJOBNAME was saved and received the ID: 9558\n", + "Al: E(hcp) - E(fcc) = 0.03 eV/atom\n" + ] + } + ], "source": [ "# Good guess\n", "out = wf(element=\"Al\", phase1=\"fcc\", phase2=\"hcp\", lattice_guess1=4.05, lattice_guess2=3)\n", @@ -1594,10 +3181,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 43, "id": "0b373764-b389-4c24-8086-f3d33a4f7fd7", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[14.829749999999995,\n", + " 15.407468749999998,\n", + " 15.999999999999998,\n", + " 16.60753125,\n", + " 17.230249999999995]" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "n = 5\n", "\n", @@ -1630,10 +3232,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 44, "id": "0dd04b4c-e3e7-4072-ad34-58f2c1e4f596", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to true, andthus could not disconnect from it.\n", + " warn(\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n" + ] + } + ], "source": [ "@Workflow.wrap_as.single_value_node()\n", "def add(a, b):\n", @@ -1678,10 +3291,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 45, "id": "2dfb967b-41ac-4463-b606-3e315e617f2a", "metadata": {}, - "outputs": [], + "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)" @@ -1689,10 +3315,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 46, "id": "2e87f858-b327-4f6b-9237-c8a557f29aeb", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.569 > 0.2\n", + "0.648 > 0.2\n", + "0.977 > 0.2\n", + "0.148 <= 0.2\n", + "Finally 0.148\n" + ] + } + ], "source": [ "@Workflow.wrap_as.single_value_node(\"random\")\n", "def random(length: int | None = None):\n", From 31c661cbfb6d5813ebd77e6a729e4afba0b128ec Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 16 Oct 2023 13:08:34 -0700 Subject: [PATCH 38/97] Allow connection copying to be more relaxed and pass over failures --- pyiron_workflow/node.py | 51 +++++++++++++++++++++++++------------ tests/unit/test_function.py | 20 ++++++++++----- 2 files changed, 49 insertions(+), 22 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index d0baf23ca..5d6076fc9 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -448,35 +448,54 @@ def get_first_shared_parent(self, other: Node) -> Composite | None: their = other return None - def copy_connections(self, other: Node) -> None: + def copy_connections( + self, + other: Node, + fail_hard: bool = True, + ) -> None: """ Copies all the connections in another node to this one. Expects the channels available on this node to be commensurate to those on the other, i.e. same label, compatible type hint for the connections that exist. This node may freely have additional channels not present in the other node. + The other node may have additional channels not present here as long as they are + not connected. + This final condition can optionally be relaxed, such that as many connections as + possible are copied, and any failures are simply overlooked. - If an exception is encountered, any connections copied so far are disconnected + If an exception is going to be raised, any connections copied so far are + disconnected first. Args: other (Node): the node whose connections should be copied. + fail_hard (bool): Whether to raise an error an exception is encountered + when trying to reproduce a connection. + + Raises: + (Exception): Any exception encountered when a connection is attempted and + fails (only when `fail_hard` is True, otherwise we `continue` past any + and all exceptions encountered). """ new_connections = [] - try: - for my_panel, other_panel in [ - (self.inputs, other.inputs), - (self.outputs, other.outputs), - (self.signals.input, other.signals.input), - (self.signals.output, other.signals.output), - ]: - for key, channel in other_panel.items(): - for target in channel.connections: + for my_panel, other_panel in [ + (self.inputs, other.inputs), + (self.outputs, other.outputs), + (self.signals.input, other.signals.input), + (self.signals.output, other.signals.output), + ]: + for key, channel in other_panel.items(): + for target in channel.connections: + try: my_panel[key].connect(target) new_connections.append((my_panel[key], target)) - except Exception as e: - # If you run into trouble, unwind what you've done - for connection in new_connections: - connection[0].disconnect(connection[1]) - raise e + except Exception as e: + if fail_hard: + # If you run into trouble, unwind what you've done + for connection in new_connections: + connection[0].disconnect(connection[1]) + raise e + else: + continue def replace_with(self, other: Node | type[Node]): """ diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 66ee5b0bd..44f4a71d5 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -421,8 +421,18 @@ def test_copy_connections(self): downstream = Function(plus_one, x=to_copy.outputs.y) upstream > to_copy > downstream - wrong_io = Function(no_default, x=upstream.outputs.y) - downstream.inputs.x.connect(wrong_io.outputs["x + y + 1"]) + wrong_io = Function( + returns_multiple, x=upstream.outputs.y, y=upstream.outputs.y + ) + downstream.inputs.x.connect(wrong_io.outputs.y) + + with self.subTest("Successful copy"): + node.copy_connections(to_copy) + self.assertIn(upstream.outputs.y, node.inputs.x.connections) + self.assertIn(upstream.signals.output.ran, node.signals.input.run) + self.assertIn(downstream.inputs.x, node.outputs.y.connections) + self.assertIn(downstream.signals.input.run, node.signals.output.ran) + node.disconnect() # Make sure you've got a clean slate with self.subTest("Ensure failed copies fail cleanly"): with self.assertRaises(AttributeError): @@ -434,12 +444,10 @@ def test_copy_connections(self): ) node.disconnect() # Make sure you've got a clean slate - with self.subTest("Successful copy"): - node.copy_connections(to_copy) + with self.subTest("Ensure that failures can be continued past"): + node.copy_connections(wrong_io, hard_connections=False) self.assertIn(upstream.outputs.y, node.inputs.x.connections) - self.assertIn(upstream.signals.output.ran, node.signals.input.run) self.assertIn(downstream.inputs.x, node.outputs.y.connections) - self.assertIn(downstream.signals.input.run, node.signals.output.ran) @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") From 43d78a39f50687ebb9a3f9c4680a89b2f8809e2e Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 16 Oct 2023 13:58:27 -0700 Subject: [PATCH 39/97] Be strict about bad connections Raise an error instead of just a warning --- pyiron_workflow/channels.py | 8 +++++--- tests/unit/test_channels.py | 24 +++++++++++------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pyiron_workflow/channels.py b/pyiron_workflow/channels.py index 7124c2631..6da1afd1f 100644 --- a/pyiron_workflow/channels.py +++ b/pyiron_workflow/channels.py @@ -178,10 +178,11 @@ class DataChannel(Channel, ABC): Type hinting is strictly enforced in one situation: when making connections to other channels and at least one data channel has a non-None value for its type hint. - In this case, we insist that the output type hint be _as or more more specific_ than + In this case, we insist that the output type hint be _as or more specific_ than the input type hint, to ensure that the input always receives output of a type it expects. This behaviour can be disabled and all connections allowed by setting `strict_connections = False` on the relevant input channel. + Attempting to make a connection breaking type hints will raise an exception. For simple type hints like `int` or `str`, type hint comparison is trivial. However, some hints take arguments, e.g. `dict[str, int]` to specify key and value @@ -258,9 +259,10 @@ def connect(self, *others: DataChannel) -> None: other.connections.append(self) else: if isinstance(other, DataChannel): - warn( + raise ValueError( f"{self.label} ({self.__class__.__name__}) and {other.label} " - f"({other.__class__.__name__}) were not a valid connection" + f"({other.__class__.__name__}) were not a valid connection. " + f"Check channel classes, type hints, etc." ) else: raise TypeError( diff --git a/tests/unit/test_channels.py b/tests/unit/test_channels.py index 3dd058111..5f51dcafe 100644 --- a/tests/unit/test_channels.py +++ b/tests/unit/test_channels.py @@ -126,19 +126,17 @@ def test_connection_validity_tests(self): "Input types should be allowed to be a super-set of output types" ) - self.no.connect(self.ni2) - self.assertNotIn( - self.no, - self.ni2.connections, - "Input types should not be allowed to be a sub-set of output types" - ) - - self.so1.connect(self.ni2) - self.assertNotIn( - self.so1, - self.ni2.connections, - "Totally different types should not allow connections" - ) + with self.assertRaises( + ValueError, + msg="Input types should not be allowed to be a sub-set of output types" + ): + self.no.connect(self.ni2) + + with self.assertRaises( + ValueError, + msg="Totally different types should not allow connections" + ): + self.so1.connect(self.ni2) self.ni2.strict_connections = False self.so1.connect(self.ni2) From a626eed3f91c36a91ff9b2a5742c00a0fe610dce Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 16 Oct 2023 13:58:56 -0700 Subject: [PATCH 40/97] Test that connection copying is handling type hints well --- tests/unit/test_function.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 44f4a71d5..dac47e954 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -434,21 +434,49 @@ def test_copy_connections(self): self.assertIn(downstream.signals.input.run, node.signals.output.ran) node.disconnect() # Make sure you've got a clean slate + def plus_one_hinted(x: int = 0) -> int: + y = x + 1 + return y + + hinted_node = Function(plus_one_hinted) + with self.subTest("Ensure failed copies fail cleanly"): - with self.assertRaises(AttributeError): + with self.assertRaises(AttributeError, msg="Wrong labels"): node.copy_connections(wrong_io) self.assertFalse( node.connected, msg="The x-input connection should have been copied, but should be " "removed when the copy fails." ) + + with self.assertRaises( + ValueError, + msg="An unhinted channel is not a valid connection for a hinted " + "channel, and should raise and exception" + ): + hinted_node.copy_connections(to_copy) + hinted_node.disconnect()# Make sure you've got a clean slate node.disconnect() # Make sure you've got a clean slate with self.subTest("Ensure that failures can be continued past"): - node.copy_connections(wrong_io, hard_connections=False) + node.copy_connections(wrong_io, fail_hard=False) self.assertIn(upstream.outputs.y, node.inputs.x.connections) self.assertIn(downstream.inputs.x, node.outputs.y.connections) + hinted_node.copy_connections(to_copy, fail_hard=False) + self.assertFalse( + hinted_node.inputs.connected, + msg="Without hard failure the copy should be allowed to proceed, but " + "we don't actually expect any connections to get copied since the " + "only one available had type hint problems" + ) + self.assertTrue( + hinted_node.outputs.connected, + msg="Without hard failure the copy should be allowed to proceed, so " + "the output should connect fine since feeding hinted to un-hinted " + "is a-ok" + ) + @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") class TestSingleValue(unittest.TestCase): From ad3e4468126e83ccbe4fa21b86d796b8fc160dfb Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 16 Oct 2023 14:08:42 -0700 Subject: [PATCH 41/97] Add method for copying data values --- pyiron_workflow/node.py | 58 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 5d6076fc9..619e6800e 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -10,10 +10,12 @@ from concurrent.futures import Future from typing import Any, Literal, Optional, TYPE_CHECKING +from pyiron_workflow.channels import NotData from pyiron_workflow.draw import Node as GraphvizNode from pyiron_workflow.files import DirectoryObject from pyiron_workflow.has_to_dict import HasToDict from pyiron_workflow.io import Signals, InputSignal, OutputSignal +from pyiron_workflow.type_hinting import valid_value from pyiron_workflow.util import SeabornColors if TYPE_CHECKING: @@ -497,6 +499,62 @@ def copy_connections( else: continue + def copy_values( + self, + other: Node, + fail_hard: bool = False, + ) -> None: + """ + Copies all the input and output data channel values in another node to this one + wherever this a channel with a matching label and compatible type hint. + Can be made more strict, so that failures to find a channel with the expected + label or finding a value not matching the type hint raise exceptions. + + If an exception is going to be raised, any values updated so far are + reverted first. + + Args: + other (Node): the node whose data values should be copied. + fail_hard (bool): Whether to raise an error an exception is encountered + when trying to duplicate a value. (Default is False, just keep going + past other's channels with no compatible label here and past values + that don't match type hints here.) + + Raises: + (Exception): Any exception encountered when copying values fails (only when + `fail_hard` is True, otherwise we `continue` past any and all + exceptions encountered). + """ + old_values = [] + for my_panel, other_panel in [ + (self.inputs, other.inputs), + (self.outputs, other.outputs), + ]: + for key, channel in other_panel.items(): + if channel.value is not NotData: + try: + hint = my_panel[key].type_hint + if hint is not None and not valid_value(channel.value, hint): + # We can be more relaxed than what we're copying, + # but not more specific else the value might be bad + raise TypeError( + f"Cannot copy value {channel.value} to {self.label} " + f"from {other.label} -- it does not match the type " + f"hint on channel {key}: {hint}." + ) + + old_value = my_panel[key].value + my_panel[key].value = channel.value + old_values.append((my_panel, key, old_value)) + except Exception as e: + if fail_hard: + # If you run into trouble, unwind what you've done + for panel, key, value in old_values: + panel[key].value = value + raise e + else: + continue + def replace_with(self, other: Node | type[Node]): """ If this node has a parent, invokes `self.parent.replace(self, other)` to swap From 56c7b6aee00c668375cb81e7286fa91b25649165 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 17 Oct 2023 14:43:18 -0700 Subject: [PATCH 42/97] Put copying right on the channels --- pyiron_workflow/channels.py | 40 +++++++++++++++++++++- tests/unit/test_channels.py | 67 +++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/channels.py b/pyiron_workflow/channels.py index 6da1afd1f..2ce1e9ab0 100644 --- a/pyiron_workflow/channels.py +++ b/pyiron_workflow/channels.py @@ -139,6 +139,24 @@ def __len__(self): def channel(self) -> Channel: return self + def copy_connections(self, other: Channel) -> None: + """ + Adds all the connections in another channel to this channel's connections. + + If an exception is encountered, all the new connections are disconnected before + the exception is raised. + """ + new_connections = [] + try: + for connect_to in other.connections: + # We do them one at a time in case any fail, so we can undo those that + # worked + self.connect(connect_to) + new_connections.append(connect_to) + except Exception as e: + self.disconnect(*new_connections) + raise e + def to_dict(self) -> dict: return { "label": self.label, @@ -240,6 +258,10 @@ def ready(self) -> bool: def _value_is_data(self): return self.value is not NotData + @property + def _has_hint(self): + return self.type_hint is not None + def connect(self, *others: DataChannel) -> None: """ For all others for which the connection is valid (one input, one output, both @@ -290,7 +312,7 @@ def _is_IO_pair(self, other: DataChannel) -> bool: return isinstance(other, DataChannel) and not isinstance(other, self.__class__) def _both_typed(self, other: DataChannel) -> bool: - return self.type_hint is not None and other.type_hint is not None + return self._has_hint and other._has_hint def _figure_out_who_is_who(self, other: DataChannel) -> (OutputData, InputData): return (self, other) if isinstance(self, OutputData) else (other, self) @@ -298,6 +320,22 @@ def _figure_out_who_is_who(self, other: DataChannel) -> (OutputData, InputData): def __str__(self): return str(self.value) + def copy_value(self, other: DataChannel) -> None: + """ + Copies the other channel's value. Unlike normal value assignment, the new value + (if it is data) must comply with this channel's type hint (if any). + """ + if ( + self._has_hint + and other._value_is_data + and not valid_value(other.value, self.type_hint) + ): + raise TypeError( + f"Channel{self.label} cannot copy value from {other.label} because " + f"value {other.value} does not match type hint {self.type_hint}" + ) + self.value = other.value + def to_dict(self) -> dict: d = super().to_dict() d["value"] = repr(self.value) diff --git a/tests/unit/test_channels.py b/tests/unit/test_channels.py index 5f51dcafe..dd2eec4d4 100644 --- a/tests/unit/test_channels.py +++ b/tests/unit/test_channels.py @@ -146,6 +146,73 @@ def test_connection_validity_tests(self): "With strict connections turned off, we should allow type-violations" ) + def test_copy_connections(self): + self.ni1.connect(self.no) + self.ni2.connect(self.no_empty) + self.ni2.copy_connections(self.ni1) + self.assertListEqual( + self.ni2.connections, + [self.no_empty, *self.ni1.connections], + msg="Copying should be additive, existing connections should still be there" + ) + + self.ni2.disconnect(*self.ni1.connections) + self.ni1.connections.append(self.so1) # Manually include a poorly-typed conn + with self.assertRaises( + ValueError, + msg="Should not be able to connect to so1 because of type hint " + "incompatibility" + ): + self.ni2.copy_connections(self.ni1) + self.assertListEqual( + self.ni2.connections, + [self.no_empty], + msg="On failing, copy should revert the copying channel to its orignial " + "state" + ) + + def test_copy_value(self): + self.ni1.value = 2 + self.ni2.copy_value(self.ni1) + self.assertEqual( + self.ni2.value, + self.ni1.value, + msg="Should be able to copy values matching type hints" + ) + + self.ni2.copy_value(self.no_empty) + self.assertIs( + self.ni2.value, + NotData, + msg="Should be able to copy values that are not-data" + ) + + with self.assertRaises( + TypeError, + msg="Should not be able to copy values of the wrong type" + ): + self.ni2.copy_value(self.so1) + + self.ni2.type_hint = None + self.ni2.copy_value(self.ni1) + self.assertEqual( + self.ni2.value, + self.ni1.value, + msg="Should be able to copy any data if we have no type hint" + ) + self.ni2.copy_value(self.so1) + self.assertEqual( + self.ni2.value, + self.so1.value, + msg="Should be able to copy any data if we have no type hint" + ) + self.ni2.copy_value(self.no_empty) + self.assertEqual( + self.ni2.value, + NotData, + msg="Should be able to copy not-data if we have no type hint" + ) + def test_ready(self): with self.subTest("Test defaults and not-data"): without_default = InputData(label="without_default", node=DummyNode()) From 2acb7638ac0082a9c254d05b85a314584ac85ba8 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 17 Oct 2023 15:03:39 -0700 Subject: [PATCH 43/97] Introduce a custom exception for invalid connections and refactor --- pyiron_workflow/channels.py | 81 ++++++++++++++++++++----------------- tests/unit/test_channels.py | 12 +++--- 2 files changed, 49 insertions(+), 44 deletions(-) diff --git a/pyiron_workflow/channels.py b/pyiron_workflow/channels.py index 2ce1e9ab0..f392e32f8 100644 --- a/pyiron_workflow/channels.py +++ b/pyiron_workflow/channels.py @@ -36,6 +36,10 @@ from pyiron_workflow.node import Node +class ChannelConnectionError(Exception): + pass + + class Channel(HasChannel, HasToDict, ABC): """ Channels facilitate the flow of information (data or control signals) into and @@ -79,14 +83,41 @@ def __str__(self): pass @abstractmethod - def connect(self, *others: Channel) -> None: + def connect(self, expected_type: type[Channel], *others: Channel) -> None: """ How to handle connections to other channels. + Children should override this method to remove `expected_type` from the + signature. + Args: + expected_type (type[Channel]): The generic type of channel expected among + others; used in raising sensible errors. *others (Channel): The other channel objects to attempt to connect with. """ - pass + for other in others: + if other in self.connections: + continue + elif self._valid_connection(other): + self.connections.append(other) + other.connections.append(self) + else: + if isinstance(other, DataChannel): + raise ChannelConnectionError( + f"{self.label} ({self.__class__.__name__}) and {other.label} " + f"({other.__class__.__name__}) were not a valid connection. " + f"Check channel classes, type hints, etc." + ) + else: + raise TypeError( + f"Can only connect two {expected_type.__name__} objects, but" + f"{self.label} ({self.__class__.__name__}) got {other} " + f"({type(other)})" + ) + + @abstractmethod + def _valid_connection(self, other: Channel) -> bool: + """Logic for determining if a connection is valid""" def disconnect(self, *others: Channel) -> list[tuple[Channel, Channel]]: """ @@ -267,33 +298,19 @@ def connect(self, *others: DataChannel) -> None: For all others for which the connection is valid (one input, one output, both data channels), adds this to the other's list of connections and the other to this list of connections. - Then the input channel gets updated with the output channel's current value. Args: *others (DataChannel): Raises: TypeError: When one of others is not a `DataChannel` + ChannelConnectionError: When the connection is deemed invalid (e.g. because + of incompatible type hints). """ - for other in others: - if self._valid_connection(other): - self.connections.append(other) - other.connections.append(self) - else: - if isinstance(other, DataChannel): - raise ValueError( - f"{self.label} ({self.__class__.__name__}) and {other.label} " - f"({other.__class__.__name__}) were not a valid connection. " - f"Check channel classes, type hints, etc." - ) - else: - raise TypeError( - f"Can only connect two channels, but {self.label} " - f"({self.__class__.__name__}) got a {other} ({type(other)})" - ) + super().connect(DataChannel, *others) def _valid_connection(self, other) -> bool: - if self._is_IO_pair(other) and not self._already_connected(other): + if self._is_IO_pair(other): if self._both_typed(other): out, inp = self._figure_out_who_is_who(other) if not inp.strict_connections: @@ -421,33 +438,21 @@ def __call__(self) -> None: def connect(self, *others: SignalChannel) -> None: """ For all others for which the connection is valid (one input, one output, both - data channels), adds this to the other's list of connections and the other to - this list of connections. + signal channels), adds this to the other's list of connections and the other to + this list of connections. Ignore others that are already in connections. Args: *others (SignalChannel): The other channels to attempt a connection to Raises: TypeError: When one of others is not a `SignalChannel` + ChannelConnectionError: When the connection is deemed invalid (e.g. because + both are output signals). """ - for other in others: - if self._valid_connection(other): - self.connections.append(other) - other.connections.append(self) - else: - if isinstance(other, SignalChannel): - warn( - f"{self.label} ({self.__class__.__name__}) and {other.label} " - f"({other.__class__.__name__}) were not a valid connection" - ) - else: - raise TypeError( - f"Can only connect two signal channels, but {self.label} " - f"({self.__class__.__name__}) got a {other} ({type(other)})" - ) + super().connect(SignalChannel, *others) def _valid_connection(self, other) -> bool: - return self._is_IO_pair(other) and not self._already_connected(other) + return self._is_IO_pair(other) def _is_IO_pair(self, other) -> bool: return isinstance(other, SignalChannel) and not isinstance( diff --git a/tests/unit/test_channels.py b/tests/unit/test_channels.py index dd2eec4d4..a92d839b8 100644 --- a/tests/unit/test_channels.py +++ b/tests/unit/test_channels.py @@ -2,7 +2,7 @@ from sys import version_info from pyiron_workflow.channels import ( - InputData, OutputData, InputSignal, OutputSignal, NotData + InputData, OutputData, InputSignal, OutputSignal, NotData, ChannelConnectionError ) @@ -127,14 +127,14 @@ def test_connection_validity_tests(self): ) with self.assertRaises( - ValueError, + ChannelConnectionError, msg="Input types should not be allowed to be a sub-set of output types" ): self.no.connect(self.ni2) with self.assertRaises( - ValueError, - msg="Totally different types should not allow connections" + ChannelConnectionError, + msg="Totally different type hints should not allow connections" ): self.so1.connect(self.ni2) @@ -159,7 +159,7 @@ def test_copy_connections(self): self.ni2.disconnect(*self.ni1.connections) self.ni1.connections.append(self.so1) # Manually include a poorly-typed conn with self.assertRaises( - ValueError, + ChannelConnectionError, msg="Should not be able to connect to so1 because of type hint " "incompatibility" ): @@ -259,7 +259,7 @@ def test_connections(self): with self.subTest("No connections to non-SignalChannels"): bad = InputData(label="numeric", node=DummyNode(), default=1, type_hint=int) - with self.assertRaises(TypeError): + with self.assertRaises(ChannelConnectionError): self.inp.connect(bad) with self.subTest("Test syntactic sugar"): From eecd699a660f49e54dc637202e0df3939fa9268c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 17 Oct 2023 15:24:53 -0700 Subject: [PATCH 44/97] Refactor and update docs --- pyiron_workflow/channels.py | 133 +++++++++++++++--------------------- tests/unit/test_channels.py | 2 +- 2 files changed, 57 insertions(+), 78 deletions(-) diff --git a/pyiron_workflow/channels.py b/pyiron_workflow/channels.py index f392e32f8..749c78554 100644 --- a/pyiron_workflow/channels.py +++ b/pyiron_workflow/channels.py @@ -46,13 +46,14 @@ class Channel(HasChannel, HasToDict, ABC): out of nodes. They must have a label and belong to a node. - Input/output channels can be (dis)connected from other output/input channels, and - store all of their current connections in a list. - This connection information is duplicated in that it is stored on _both_ channels - that form the connection. + Input/output channels can be (dis)connected from other output/input channels of the + same generic type (i.e. data or signal), and store all of their current connections + in a list. + This connection information is reflexive, and is duplicated to be stored on _both_ + channels in the form of a reference to their counterpart in the connection. - Child classes must define a string representation, `__str__`, and what to do on an - attempted connection, `connect`. + Child classes must define a string representation, `__str__`, and their + `generic_type` which is a parent of both themselves and their output/input partner. Attributes: label (str): The name of the channel. @@ -82,18 +83,38 @@ def __init__( def __str__(self): pass + @property @abstractmethod - def connect(self, expected_type: type[Channel], *others: Channel) -> None: + def generic_type(self) -> type[Channel]: + """Input and output class pairs should share this parent class""" + + def _valid_connection(self, other: Channel) -> bool: + """ + Logic for determining if a connection is valid. + + Connections should have the same generic type, but not the same type -- i.e. + they should be an input/output pair of some connection type. + """ + return ( + isinstance(other, self.generic_type) + and not isinstance(other, self.__class__) + ) + + def connect(self, *others: Channel) -> None: """ - How to handle connections to other channels. + Form a connection between this and one or more other channels. + Connections are reflexive, and must occur between input and output channels of + the same `generic_type` (i.e. data or signal). - Children should override this method to remove `expected_type` from the - signature. Args: - expected_type (type[Channel]): The generic type of channel expected among - others; used in raising sensible errors. *others (Channel): The other channel objects to attempt to connect with. + + Raises: + (ChannelConnectionError): If the other channel is of the correct generic + type, but nonetheless not a valid connection. + (TypeError): If the other channel is not an instance of this channel's + generic type. """ for other in others: if other in self.connections: @@ -102,23 +123,20 @@ def connect(self, expected_type: type[Channel], *others: Channel) -> None: self.connections.append(other) other.connections.append(self) else: - if isinstance(other, DataChannel): + if isinstance(other, self.generic_type): raise ChannelConnectionError( f"{self.label} ({self.__class__.__name__}) and {other.label} " - f"({other.__class__.__name__}) were not a valid connection. " - f"Check channel classes, type hints, etc." + f"({other.__class__.__name__}) share a generic type but were " + f"not a valid connection. Check channel classes, type hints, " + f"etc." ) else: raise TypeError( - f"Can only connect two {expected_type.__name__} objects, but" - f"{self.label} ({self.__class__.__name__}) got {other} " + f"Can only connect two {self.generic_type.__name__} objects, " + f"but {self.label} ({self.__class__.__name__}) got {other} " f"({type(other)})" ) - @abstractmethod - def _valid_connection(self, other: Channel) -> bool: - """Logic for determining if a connection is valid""" - def disconnect(self, *others: Channel) -> list[tuple[Channel, Channel]]: """ If currently connected to any others, removes this and the other from eachothers @@ -157,9 +175,6 @@ def connected(self) -> bool: """ return len(self.connections) > 0 - def _already_connected(self, other: Channel) -> bool: - return other in self.connections - def __iter__(self): return self.connections.__iter__() @@ -221,17 +236,16 @@ class DataChannel(Channel, ABC): (In the future they may optionally have a storage history limit.) (In the future they may optionally have an ontological type.) - The `value` held by a channel can be manually assigned, but should normally be set - by the `update` method. - In neither case is the type hint strictly enforced. + Note that for the sake of computational efficiency, assignments to the `value` + property are not type-checked; type-checking occurs only for connections where both + channels have a type hint, and when a value is being copied from another channel + with the `copy_value` method. - Type hinting is strictly enforced in one situation: when making connections to - other channels and at least one data channel has a non-None value for its type hint. - In this case, we insist that the output type hint be _as or more specific_ than - the input type hint, to ensure that the input always receives output of a type it - expects. This behaviour can be disabled and all connections allowed by setting - `strict_connections = False` on the relevant input channel. - Attempting to make a connection breaking type hints will raise an exception. + When type checking channel connections, we insist that the output type hint be + _as or more specific_ than the input type hint, to ensure that the input always + receives output of a type it expects. This behaviour can be disabled and all + connections allowed by setting `strict_connections = False` on the relevant input + channel. For simple type hints like `int` or `str`, type hint comparison is trivial. However, some hints take arguments, e.g. `dict[str, int]` to specify key and value @@ -272,6 +286,10 @@ def __init__( self.value = default self.type_hint = type_hint + @property + def generic_type(self) -> type[Channel]: + return DataChannel + @property def ready(self) -> bool: """ @@ -293,24 +311,8 @@ def _value_is_data(self): def _has_hint(self): return self.type_hint is not None - def connect(self, *others: DataChannel) -> None: - """ - For all others for which the connection is valid (one input, one output, both - data channels), adds this to the other's list of connections and the other to - this list of connections. - - Args: - *others (DataChannel): - - Raises: - TypeError: When one of others is not a `DataChannel` - ChannelConnectionError: When the connection is deemed invalid (e.g. because - of incompatible type hints). - """ - super().connect(DataChannel, *others) - def _valid_connection(self, other) -> bool: - if self._is_IO_pair(other): + if super()._valid_connection(other): if self._both_typed(other): out, inp = self._figure_out_who_is_who(other) if not inp.strict_connections: @@ -325,9 +327,6 @@ def _valid_connection(self, other) -> bool: else: return False - def _is_IO_pair(self, other: DataChannel) -> bool: - return isinstance(other, DataChannel) and not isinstance(other, self.__class__) - def _both_typed(self, other: DataChannel) -> bool: return self._has_hint and other._has_hint @@ -435,29 +434,9 @@ class SignalChannel(Channel, ABC): def __call__(self) -> None: pass - def connect(self, *others: SignalChannel) -> None: - """ - For all others for which the connection is valid (one input, one output, both - signal channels), adds this to the other's list of connections and the other to - this list of connections. Ignore others that are already in connections. - - Args: - *others (SignalChannel): The other channels to attempt a connection to - - Raises: - TypeError: When one of others is not a `SignalChannel` - ChannelConnectionError: When the connection is deemed invalid (e.g. because - both are output signals). - """ - super().connect(SignalChannel, *others) - - def _valid_connection(self, other) -> bool: - return self._is_IO_pair(other) - - def _is_IO_pair(self, other) -> bool: - return isinstance(other, SignalChannel) and not isinstance( - other, self.__class__ - ) + @property + def generic_type(self) -> type[Channel]: + return SignalChannel def connect_output_signal(self, signal: OutputSignal): self.connect(signal) diff --git a/tests/unit/test_channels.py b/tests/unit/test_channels.py index a92d839b8..f747350c5 100644 --- a/tests/unit/test_channels.py +++ b/tests/unit/test_channels.py @@ -259,7 +259,7 @@ def test_connections(self): with self.subTest("No connections to non-SignalChannels"): bad = InputData(label="numeric", node=DummyNode(), default=1, type_hint=int) - with self.assertRaises(ChannelConnectionError): + with self.assertRaises(TypeError): self.inp.connect(bad) with self.subTest("Test syntactic sugar"): From 28f89f0f2d3c772ca69220be0636406d8fa9dfeb Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 17 Oct 2023 15:27:12 -0700 Subject: [PATCH 45/97] Use the new custom error in function tests --- tests/unit/test_function.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index dac47e954..595beee54 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -9,7 +9,7 @@ from pyiron_workflow.executors import CloudpickleProcessPoolExecutor as Executor -from pyiron_workflow.channels import NotData +from pyiron_workflow.channels import NotData, ChannelConnectionError from pyiron_workflow.files import DirectoryObject from pyiron_workflow.function import ( Function, SingleValue, function_node, single_value_node @@ -450,7 +450,7 @@ def plus_one_hinted(x: int = 0) -> int: ) with self.assertRaises( - ValueError, + ChannelConnectionError, msg="An unhinted channel is not a valid connection for a hinted " "channel, and should raise and exception" ): From 8f02e72a22e5b0c812fdf501e41a9bcc050f10c1 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 17 Oct 2023 15:50:19 -0700 Subject: [PATCH 46/97] Use new method in copying values --- pyiron_workflow/node.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 619e6800e..cd25ef25d 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -462,8 +462,6 @@ def copy_connections( This node may freely have additional channels not present in the other node. The other node may have additional channels not present here as long as they are not connected. - This final condition can optionally be relaxed, such that as many connections as - possible are copied, and any failures are simply overlooked. If an exception is going to be raised, any connections copied so far are disconnected first. @@ -530,27 +528,17 @@ def copy_values( (self.inputs, other.inputs), (self.outputs, other.outputs), ]: - for key, channel in other_panel.items(): - if channel.value is not NotData: + for key, to_copy in other_panel.items(): + if to_copy.value is not NotData: try: - hint = my_panel[key].type_hint - if hint is not None and not valid_value(channel.value, hint): - # We can be more relaxed than what we're copying, - # but not more specific else the value might be bad - raise TypeError( - f"Cannot copy value {channel.value} to {self.label} " - f"from {other.label} -- it does not match the type " - f"hint on channel {key}: {hint}." - ) - old_value = my_panel[key].value - my_panel[key].value = channel.value - old_values.append((my_panel, key, old_value)) + my_panel[key].copy_value(to_copy.value) + old_values.append((my_panel[key], old_value)) except Exception as e: if fail_hard: # If you run into trouble, unwind what you've done - for panel, key, value in old_values: - panel[key].value = value + for channel, value in old_values: + channel.value = value raise e else: continue From 009cbada54a054ab54c093825795f77357e3cfb0 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 17 Oct 2023 15:50:27 -0700 Subject: [PATCH 47/97] Add return values for reversion and update docs --- pyiron_workflow/node.py | 44 +++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index cd25ef25d..cdeb2c8e3 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -21,6 +21,7 @@ if TYPE_CHECKING: import graphviz + from pyiron_workflow.channels import Channel from pyiron_workflow.composite import Composite from pyiron_workflow.io import IO, Inputs, Outputs @@ -454,11 +455,18 @@ def copy_connections( self, other: Node, fail_hard: bool = True, - ) -> None: + ) -> list[tuple[Channel, Channel]]: """ Copies all the connections in another node to this one. - Expects the channels available on this node to be commensurate to those on the - other, i.e. same label, compatible type hint for the connections that exist. + Expects all connected channels on the other node to have a counterpart on this + node -- i.e. the same label, type, and (for data) a type hint compatible with + all the existing connections being copied. + This requirement can be optionally relaxed such that any failures encountered + when attempting to make a connection (i.e. this node has no channel with a + corresponding label as the other node, or the new connection fails its validity + check), such that we simply continue past these errors and make as many + connections as we can while ignoring errors. + This node may freely have additional channels not present in the other node. The other node may have additional channels not present here as long as they are not connected. @@ -469,12 +477,12 @@ def copy_connections( Args: other (Node): the node whose connections should be copied. fail_hard (bool): Whether to raise an error an exception is encountered - when trying to reproduce a connection. + when trying to reproduce a connection. (Default is True; revert new + connections then raise the exception.) - Raises: - (Exception): Any exception encountered when a connection is attempted and - fails (only when `fail_hard` is True, otherwise we `continue` past any - and all exceptions encountered). + Returns: + list[tuple[Channel, Channel]]: A list of all the newly created connection + pairs (for reverting changes). """ new_connections = [] for my_panel, other_panel in [ @@ -496,17 +504,19 @@ def copy_connections( raise e else: continue + return new_connections def copy_values( self, other: Node, fail_hard: bool = False, - ) -> None: + ) -> list[tuple[Channel, Any]]: """ - Copies all the input and output data channel values in another node to this one - wherever this a channel with a matching label and compatible type hint. - Can be made more strict, so that failures to find a channel with the expected - label or finding a value not matching the type hint raise exceptions. + Copies all data from input and output channels in the other node onto this one. + Ignores other channels that hold non-data. + Failures to find a corresponding channel on this node (matching label, type, and + compatible type hint) are ignored by default, but can optionally be made to + raise an exception. If an exception is going to be raised, any values updated so far are reverted first. @@ -518,10 +528,9 @@ def copy_values( past other's channels with no compatible label here and past values that don't match type hints here.) - Raises: - (Exception): Any exception encountered when copying values fails (only when - `fail_hard` is True, otherwise we `continue` past any and all - exceptions encountered). + Returns: + list[tuple[Channel, Any]]: A list of tuples giving channels whose value has + been updated and what it used to be (for reverting changes). """ old_values = [] for my_panel, other_panel in [ @@ -542,6 +551,7 @@ def copy_values( raise e else: continue + return old_values def replace_with(self, other: Node | type[Node]): """ From e82ed4ce19aa80be3e207871097db5c23a3c4a94 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 17 Oct 2023 16:21:18 -0700 Subject: [PATCH 48/97] :bug: pass the right object --- pyiron_workflow/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index cdeb2c8e3..52d37422b 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -541,7 +541,7 @@ def copy_values( if to_copy.value is not NotData: try: old_value = my_panel[key].value - my_panel[key].copy_value(to_copy.value) + my_panel[key].copy_value(to_copy) old_values.append((my_panel[key], old_value)) except Exception as e: if fail_hard: From 8ee718229e2def26fed60590cf741a64a26fc39c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 17 Oct 2023 16:21:26 -0700 Subject: [PATCH 49/97] Add tests for value copying --- tests/unit/test_function.py | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 595beee54..bcecb5e4e 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -477,6 +477,74 @@ def plus_one_hinted(x: int = 0) -> int: "is a-ok" ) + def test_copy_values(self): + @function_node() + def reference(x=0, y: int = 0, z: int | float = 0, omega=None, extra_here=None): + out = 42 + return out + + @function_node() + def all_floats(x=1.1, y=1.1, z=1.1, omega=NotData, extra_there=None) -> float: + out = 42.1 + return out + + # Instantiate the nodes and run them (so they have output data too) + ref = reference() + floats = all_floats() + ref() + floats() + + ref.copy_values(floats) + self.assertEqual( + ref.inputs.x.value, + 1.1, + msg="Untyped channels should copy freely" + ) + self.assertEqual( + ref.inputs.y.value, + 0, + msg="Typed channels should ignore values where the type check fails" + ) + self.assertEqual( + ref.inputs.z.value, + 1.1, + msg="Typed channels should copy values that conform to their hint" + ) + self.assertEqual( + ref.inputs.omega.value, + None, + msg="NotData should be ignored when copying" + ) + self.assertEqual( + ref.outputs.out.value, + 42.1, + msg="Output data should also get copied" + ) + # Note also that these nodes each have extra channels the other doesn't that + # are simply ignored + + @function_node() + def extra_channel(x=1, y=1, z=1, not_present=42): + out = 42 + return out + + extra = extra_channel() + extra() + + ref.inputs.x = 0 # Revert the value + with self.assertRaises( + TypeError, + msg="Type hint should prevent update when we fail hard" + ): + ref.copy_values(floats, fail_hard=True) + + ref.copy_values(extra) # No problem + with self.assertRaises( + AttributeError, + msg="Missing a channel that holds data is also grounds for failure" + ): + ref.copy_values(extra, fail_hard=True) + @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") class TestSingleValue(unittest.TestCase): From 4a5bb9419993053f24ebf4af9d6c50622a65668e Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 17 Oct 2023 16:31:47 -0700 Subject: [PATCH 50/97] Provide a public interface that copies both at once And make the individual copiers private --- pyiron_workflow/composite.py | 2 +- pyiron_workflow/node.py | 43 ++++++++++++++++++++++++++++++++---- tests/unit/test_function.py | 18 +++++++-------- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index b7c5fc787..464c3546e 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -420,7 +420,7 @@ def replace(self, owned_node: Node | str, replacement: Node | type[Node]): f"got {replacement}" ) - replacement.copy_connections(owned_node) + replacement._copy_connections(owned_node) replacement.label = owned_node.label is_starting_node = owned_node in self.starting_nodes self.remove(owned_node) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 52d37422b..26c50b128 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -451,7 +451,42 @@ def get_first_shared_parent(self, other: Node) -> Composite | None: their = other return None - def copy_connections( + def copy_io( + self, + other: Node, + connections_fail_hard: bool = True, + values_fail_hard: bool = False, + ) -> None: + """ + Copies connections and values from another node's IO onto this node's IO. + Other channels with no connections are ignored for copying connections, and all + data channels without data are ignored for copying data. + Otherwise, default behaviour is to throw an exception if any of the other node's + connections fail to copy, but failed value copies are simply ignored (e.g. + because this node does not have a channel with a commensurate label or the + value breaks a type hint). + This error throwing/passing behaviour can be controlled with boolean flags. + + In the case that an exception is thrown, all newly formed connections are broken + and any new values are reverted to their old state before the exception is + raised. + + Args: + other (Node): The other node whose IO to copy. + connections_fail_hard: Whether to raise exceptions encountered when copying + connections. (Default is True.) + values_fail_hard (bool): Whether to raise exceptions encountered when + copying values. (Default is False.) + """ + new_connections = self._copy_connections(other, fail_hard=connections_fail_hard) + try: + self._copy_values(other, fail_hard=values_fail_hard) + except Exception as e: + for this, other in new_connections: + this.disconnect(other) + raise e + + def _copy_connections( self, other: Node, fail_hard: bool = True, @@ -499,14 +534,14 @@ def copy_connections( except Exception as e: if fail_hard: # If you run into trouble, unwind what you've done - for connection in new_connections: - connection[0].disconnect(connection[1]) + for this, other in new_connections: + this.disconnect(other) raise e else: continue return new_connections - def copy_values( + def _copy_values( self, other: Node, fail_hard: bool = False, diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index bcecb5e4e..01cf516d4 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -427,7 +427,7 @@ def test_copy_connections(self): downstream.inputs.x.connect(wrong_io.outputs.y) with self.subTest("Successful copy"): - node.copy_connections(to_copy) + node._copy_connections(to_copy) self.assertIn(upstream.outputs.y, node.inputs.x.connections) self.assertIn(upstream.signals.output.ran, node.signals.input.run) self.assertIn(downstream.inputs.x, node.outputs.y.connections) @@ -442,7 +442,7 @@ def plus_one_hinted(x: int = 0) -> int: with self.subTest("Ensure failed copies fail cleanly"): with self.assertRaises(AttributeError, msg="Wrong labels"): - node.copy_connections(wrong_io) + node._copy_connections(wrong_io) self.assertFalse( node.connected, msg="The x-input connection should have been copied, but should be " @@ -454,16 +454,16 @@ def plus_one_hinted(x: int = 0) -> int: msg="An unhinted channel is not a valid connection for a hinted " "channel, and should raise and exception" ): - hinted_node.copy_connections(to_copy) + hinted_node._copy_connections(to_copy) hinted_node.disconnect()# Make sure you've got a clean slate node.disconnect() # Make sure you've got a clean slate with self.subTest("Ensure that failures can be continued past"): - node.copy_connections(wrong_io, fail_hard=False) + node._copy_connections(wrong_io, fail_hard=False) self.assertIn(upstream.outputs.y, node.inputs.x.connections) self.assertIn(downstream.inputs.x, node.outputs.y.connections) - hinted_node.copy_connections(to_copy, fail_hard=False) + hinted_node._copy_connections(to_copy, fail_hard=False) self.assertFalse( hinted_node.inputs.connected, msg="Without hard failure the copy should be allowed to proceed, but " @@ -494,7 +494,7 @@ def all_floats(x=1.1, y=1.1, z=1.1, omega=NotData, extra_there=None) -> float: ref() floats() - ref.copy_values(floats) + ref._copy_values(floats) self.assertEqual( ref.inputs.x.value, 1.1, @@ -536,14 +536,14 @@ def extra_channel(x=1, y=1, z=1, not_present=42): TypeError, msg="Type hint should prevent update when we fail hard" ): - ref.copy_values(floats, fail_hard=True) + ref._copy_values(floats, fail_hard=True) - ref.copy_values(extra) # No problem + ref._copy_values(extra) # No problem with self.assertRaises( AttributeError, msg="Missing a channel that holds data is also grounds for failure" ): - ref.copy_values(extra, fail_hard=True) + ref._copy_values(extra, fail_hard=True) @unittest.skipUnless(version_info[0] == 3 and version_info[1] >= 10, "Only supported for 3.10+") From 82c38f25ce40a1c050d15bcdb9a45c9fcc5a91a7 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 17 Oct 2023 16:34:09 -0700 Subject: [PATCH 51/97] Use the new public interface when composite is replacing a node --- pyiron_workflow/composite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 464c3546e..cd1132593 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -420,7 +420,7 @@ def replace(self, owned_node: Node | str, replacement: Node | type[Node]): f"got {replacement}" ) - replacement._copy_connections(owned_node) + replacement.copy_io(owned_node) replacement.label = owned_node.label is_starting_node = owned_node in self.starting_nodes self.remove(owned_node) From ce3ba335c6849ea72eafafdd9032e9a91374673d Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 17 Oct 2023 16:34:23 -0700 Subject: [PATCH 52/97] Rerun the demo notebook Just to make sure; it all ran fine --- notebooks/workflow_example.ipynb | 328 ++++++++++++++++--------------- 1 file changed, 169 insertions(+), 159 deletions(-) diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index 02ba8d4ff..06e536ed2 100644 --- a/notebooks/workflow_example.ipynb +++ b/notebooks/workflow_example.ipynb @@ -666,8 +666,8 @@ { "data": { "text/plain": [ - "array([0.87397076, 0.3156643 , 0.76573646, 0.6628496 , 0.98520977,\n", - " 0.66537535, 0.77108864, 0.64995335, 0.6862261 , 0.43859525])" + "array([0.1153697 , 0.29712504, 0.22636199, 0.1263152 , 0.00630191,\n", + " 0.64039423, 0.73223408, 0.76977259, 0.62491999, 0.52663026])" ] }, "execution_count": 23, @@ -676,7 +676,7 @@ }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiMAAAGdCAYAAADAAnMpAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAnfElEQVR4nO3dcXDU9Z3/8dcmIdlgk/USjmQRigtVJOYqJJlAoFznqgTQy0mnHdN6gHrqNJw9BE6vUO5Mw3UmU69nlWqiKNGxUOQOxB/MpTkzY4UgWA4ITtPQ4kHaBNmYSTg3sTZBks/vDybRNYnku2T3s5s8HzPfP/aTz3f3vZ9Z2Nd8Pt/vZ13GGCMAAABL4mwXAAAAxjfCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrEmwXMBJ9fX06f/68UlJS5HK5bJcDAABGwBijrq4uTZkyRXFxw89/xEQYOX/+vKZNm2a7DAAAEIKWlhZNnTp12L/HRBhJSUmRdPnNpKamWq4GAACMRGdnp6ZNmzbwPT6cmAgj/UszqamphBEAAGLMlS6x4AJWAABgFWEEAABY5TiMHDx4UEVFRZoyZYpcLpdee+21K55z4MAB5ebmyu12a8aMGXr22WdDqRUAAIxBjsPIH//4R91yyy16+umnR9S/qalJt99+uxYtWqT6+np9//vf15o1a7Rnzx7HxQIAgLHH8QWsy5Yt07Jly0bc/9lnn9UXv/hFPfnkk5Kk2bNn69ixY/rxj3+sb3zjG05fHgAAjDFhv2bkyJEjKiwsDGpbsmSJjh07po8//njIc3p6etTZ2Rl0AACAsSnsYaS1tVUZGRlBbRkZGbp06ZLa29uHPKe8vFwej2fgYMMzAADGrojcTfPZ+4uNMUO299u4caMCgcDA0dLSEvYaAQCAHWHf9CwzM1Otra1BbW1tbUpISFB6evqQ5yQlJSkpKSncpQEAxqDePqOjTRfU1tWtySlu5fvSFB/H75pFs7CHkYKCAu3fvz+o7fXXX1deXp4mTJgQ7pcHAIwjNQ1+le1vlD/QPdDm9bhVWpSlpdlei5Xh8zhepvnwww918uRJnTx5UtLlW3dPnjyp5uZmSZeXWFatWjXQv6SkRH/4wx+0fv16nTp1SlVVVdq2bZseeeSR0XkHAADochBZvf1EUBCRpNZAt1ZvP6GaBr+lynAljsPIsWPHNHfuXM2dO1eStH79es2dO1ePPfaYJMnv9w8EE0ny+Xyqrq7Wm2++qTlz5uhf//VftWXLFm7rBQCMmt4+o7L9jTJD/K2/rWx/o3r7huoB21ym/2rSKNbZ2SmPx6NAIMAP5QEABjlypkPffv7tK/bb+eB8Fcwc+npFjL6Rfn/z2zQAgJjX1tV95U4O+iGyCCMAgJg3OcU9qv0QWYQRAEDMy/elyetxa7gbeF26fFdNvi8tkmVhhAgjAICYFx/nUmlRliQNCiT9j0uLsthvJEoRRgAAY8LSbK8qV+Qo0xO8FJPpcatyRQ77jESxsG96BgBApCzN9mpxViY7sMYYwggAYEyJj3Nx+26MYZkGAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYlWC7AFt6+4yONl1QW1e3Jqe4le9LU3ycy3ZZAACMO+MyjNQ0+FW2v1H+QPdAm9fjVmlRlpZmey1WBgDA+DPulmlqGvxavf1EUBCRpNZAt1ZvP6GaBr+lygAAGJ/GVRjp7TMq298oM8Tf+tvK9jeqt2+oHgAAIBzGVRg52nRh0IzIpxlJ/kC3jjZdiFxRAACMc+MqjLR1DR9EQukHAACu3rgKI5NT3KPaDwAAXL1xFUbyfWnyetwa7gZely7fVZPvS4tkWQAAjGvjKozEx7lUWpQlSYMCSf/j0qIs9hsBACCCxlUYkaSl2V5VrshRpid4KSbT41blihz2GQEAIMLG5aZnS7O9WpyVyQ6sAABEgXEZRqTLSzYFM9NtlwEAwLg37pZpAABAdCGMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAq0IKIxUVFfL5fHK73crNzVVdXd3n9t+xY4duueUWTZw4UV6vV/fdd586OjpCKhgAAIwtjsPIrl27tHbtWm3atEn19fVatGiRli1bpubm5iH7Hzp0SKtWrdL999+v3/zmN/rP//xP/c///I8eeOCBqy4eAADEPsdh5IknntD999+vBx54QLNnz9aTTz6padOmqbKycsj+b7/9tq6//nqtWbNGPp9PX/nKV/Sd73xHx44du+riAQBA7HMURi5evKjjx4+rsLAwqL2wsFCHDx8e8pwFCxbo3Llzqq6uljFG77//vnbv3q077rhj2Nfp6elRZ2dn0AEAAMYmR2Gkvb1dvb29ysjICGrPyMhQa2vrkOcsWLBAO3bsUHFxsRITE5WZmalrr71WP/3pT4d9nfLycnk8noFj2rRpTsoEAAAxJKQLWF0uV9BjY8ygtn6NjY1as2aNHnvsMR0/flw1NTVqampSSUnJsM+/ceNGBQKBgaOlpSWUMgEAQAxIcNJ50qRJio+PHzQL0tbWNmi2pF95ebkWLlyoRx99VJL05S9/Wddcc40WLVqkH/7wh/J6vYPOSUpKUlJSkpPSAABAjHI0M5KYmKjc3FzV1tYGtdfW1mrBggVDnvPRRx8pLi74ZeLj4yVdnlEBAADjm+NlmvXr1+uFF15QVVWVTp06pXXr1qm5uXlg2WXjxo1atWrVQP+ioiK9+uqrqqys1NmzZ/XWW29pzZo1ys/P15QpU0bvnQAAgJjkaJlGkoqLi9XR0aHNmzfL7/crOztb1dXVmj59uiTJ7/cH7Tly7733qqurS08//bT+8R//Uddee62+9rWv6Uc/+tHovQsAABCzXCYG1ko6Ozvl8XgUCASUmppquxwAADACI/3+5rdpAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYFWC7QIA4Ep6+4yONl1QW1e3Jqe4le9LU3ycy3ZZAEYJYQRAVKtp8Ktsf6P8ge6BNq/HrdKiLC3N9lqsDMBoYZkGQNSqafBr9fYTQUFEkloD3Vq9/YRqGvyWKgMwmggjAKJSb59R2f5GmSH+1t9Wtr9RvX1D9QAQSwgjAKLS0aYLg2ZEPs1I8ge6dbTpQuSKAhAWhBEAUamta/ggEko/ANGLMAIgKk1OcY9qPwDRizACICrl+9Lk9bg13A28Ll2+qybflxbJsgCEAWEEQFSKj3OptChLkgYFkv7HpUVZ7DcCjAGEEQBRa2m2V5UrcpTpCV6KyfS4Vbkih31GgDGCTc8ARLWl2V4tzspkB1ZgDCOMAIh68XEuFcxMt10GgDAJaZmmoqJCPp9Pbrdbubm5qqur+9z+PT092rRpk6ZPn66kpCTNnDlTVVVVIRUMAADGFsczI7t27dLatWtVUVGhhQsX6rnnntOyZcvU2NioL37xi0Oec9ddd+n999/Xtm3b9KUvfUltbW26dOnSVRcPAABin8sY42gv5Xnz5iknJ0eVlZUDbbNnz9by5ctVXl4+qH9NTY2+9a1v6ezZs0pLC+0WvM7OTnk8HgUCAaWmpob0HAAAILJG+v3taJnm4sWLOn78uAoLC4PaCwsLdfjw4SHP2bdvn/Ly8vT444/ruuuu04033qhHHnlEf/rTn4Z9nZ6eHnV2dgYdAABgbHK0TNPe3q7e3l5lZGQEtWdkZKi1tXXIc86ePatDhw7J7XZr7969am9v19///d/rwoULw143Ul5errKyMielAQCAGBXSBawuV/AtdcaYQW39+vr65HK5tGPHDuXn5+v222/XE088oZdeemnY2ZGNGzcqEAgMHC0tLaGUCQAAYoCjmZFJkyYpPj5+0CxIW1vboNmSfl6vV9ddd508Hs9A2+zZs2WM0blz53TDDTcMOicpKUlJSUlOSgMAADHK0cxIYmKicnNzVVtbG9ReW1urBQsWDHnOwoULdf78eX344YcDbadPn1ZcXJymTp0aQskAAGAscbxMs379er3wwguqqqrSqVOntG7dOjU3N6ukpETS5SWWVatWDfS/++67lZ6ervvuu0+NjY06ePCgHn30Uf3d3/2dkpOTR++dAACAmOR4n5Hi4mJ1dHRo8+bN8vv9ys7OVnV1taZPny5J8vv9am5uHuj/hS98QbW1tfqHf/gH5eXlKT09XXfddZd++MMfjt67AAAAMcvxPiM2sM8IAACxJyz7jAAAAIw2wggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwKsF2AQDGr94+o6NNF9TW1a3JKW7l+9IUH+eyXRaACCOMALCipsGvsv2N8ge6B9q8HrdKi7K0NNtrsTIAkcYyDYCIq2nwa/X2E0FBRJJaA91avf2Eahr8lioDYANhBEBE9fYZle1vlBnib/1tZfsb1ds3VA8AYxFhBEBEHW26MGhG5NOMJH+gW0ebLkSuKABWEUYARFRb1/BBJJR+AGIfYQRARE1OcY9qPwCxjzACIKLyfWnyetwa7gZely7fVZPvS4tkWQAsIowAiKj4OJdKi7IkaVAg6X9cWpTFfiPAOEIYARBxS7O9qlyRo0xP8FJMpsetyhU57DMCjDNsegbAiqXZXi3OymQHVgCEEQD2xMe5VDAz3XYZACxjmQYAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVIYWRiooK+Xw+ud1u5ebmqq6ubkTnvfXWW0pISNCcOXNCeVkAADAGOQ4ju3bt0tq1a7Vp0ybV19dr0aJFWrZsmZqbmz/3vEAgoFWrVunWW28NuVgAADD2uIwxxskJ8+bNU05OjiorKwfaZs+ereXLl6u8vHzY8771rW/phhtuUHx8vF577TWdPHlyxK/Z2dkpj8ejQCCg1NRUJ+UCAABLRvr97Whm5OLFizp+/LgKCwuD2gsLC3X48OFhz3vxxRd15swZlZaWOnk5AAAwDiQ46dze3q7e3l5lZGQEtWdkZKi1tXXIc959911t2LBBdXV1SkgY2cv19PSop6dn4HFnZ6eTMgEAQAwJ6QJWl8sV9NgYM6hNknp7e3X33XerrKxMN95444ifv7y8XB6PZ+CYNm1aKGUCAIAY4CiMTJo0SfHx8YNmQdra2gbNlkhSV1eXjh07pu9+97tKSEhQQkKCNm/erHfeeUcJCQl64403hnydjRs3KhAIDBwtLS1OygQAADHE0TJNYmKicnNzVVtbq69//esD7bW1tbrzzjsH9U9NTdWvf/3roLaKigq98cYb2r17t3w+35Cvk5SUpKSkJCelAQCAGOUojEjS+vXrtXLlSuXl5amgoEBbt25Vc3OzSkpKJF2e1Xjvvff08ssvKy4uTtnZ2UHnT548WW63e1A7AAAYnxyHkeLiYnV0dGjz5s3y+/3Kzs5WdXW1pk+fLkny+/1X3HMEAACgn+N9RmxgnxEAAGJPWPYZAQAAGG2EEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFY5/tVeAIiU3j6jo00X1NbVrckpbuX70hQf57JdFoBRRhgBEJVqGvwq298of6B7oM3rcau0KEtLs70WKwMw2limARB1ahr8Wr39RFAQkaTWQLdWbz+hmga/pcoAhANhBEBU6e0zKtvfKDPE3/rbyvY3qrdvqB4AYhFhBEBUOdp0YdCMyKcZSf5At442XYhcUQDCijACIKq0dQ0fRELpByD6EUYARJXJKe5R7Qcg+hFGAESVfF+avB63hruB16XLd9Xk+9IiWRaAMCKMAIgq8XEulRZlSdKgQNL/uLQoi/1GgDGEMAIg6izN9qpyRY4yPcFLMZketypX5LDPCDDGsOkZgKi0NNurxVmZ7MAKjAOEEQBRKz7OpYKZ6bbLABBmLNMAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsSrBdAAAAsKO3z+ho0wW1dXVrcopb+b40xce5Il4HYQQAgHGopsGvsv2N8ge6B9q8HrdKi7K0NNsb0VpYpgEAYJypafBr9fYTQUFEkloD3Vq9/YRqGvwRrYcwAgAxrLfP6MiZDv2/k+/pyJkO9fYZ2yUhyvX2GZXtb9RQn5T+trL9jRH9LLFMAwAxKpqm2RE7jjZdGDQj8mlGkj/QraNNF1QwMz0iNTEzAgAxKNqm2RE72rqGDyKh9BsNhBEAiDHROM2O2DE5xT2q/UYDYQQAYoyTaXbgs/J9afJ63BruBl6XLi/35fvSIlYTYQQAYkw0TrMjdsTHuVRalCVJgwJJ/+PSoqyI7jdCGAGAGBON0+yILUuzvapckaNMT/BnJNPjVuWKnNjYZ6SiokI+n09ut1u5ubmqq6sbtu+rr76qxYsX68///M+VmpqqgoIC/fd//3fIBQPAeBeN0+yIPUuzvTr0va9p54Pz9dS35mjng/N16Htfs3InluMwsmvXLq1du1abNm1SfX29Fi1apGXLlqm5uXnI/gcPHtTixYtVXV2t48eP66/+6q9UVFSk+vr6qy4eAMajaJxmR2yKj3OpYGa67pxznQpmplv7zLiMMY4ut543b55ycnJUWVk50DZ79mwtX75c5eXlI3qOm2++WcXFxXrsscdG1L+zs1Mej0eBQECpqalOygWAMYt9RhDtRvr97WjTs4sXL+r48ePasGFDUHthYaEOHz48oufo6+tTV1eX0tKYPgSAq7E026vFWZlR8UNnwNVwFEba29vV29urjIyMoPaMjAy1traO6Dn+/d//XX/84x911113Ddunp6dHPT09A487OzudlAkA40b/NDsQy0K6gNXlCk7dxphBbUPZuXOnfvCDH2jXrl2aPHnysP3Ky8vl8XgGjmnTpoVSJgAAiAGOwsikSZMUHx8/aBakra1t0GzJZ+3atUv333+//uM//kO33Xbb5/bduHGjAoHAwNHS0uKkTAAAEEMchZHExETl5uaqtrY2qL22tlYLFiwY9rydO3fq3nvv1c9//nPdcccdV3ydpKQkpaamBh0AAGBscvyrvevXr9fKlSuVl5engoICbd26Vc3NzSopKZF0eVbjvffe08svvyzpchBZtWqVnnrqKc2fP39gViU5OVkej2cU3woAAIhFjsNIcXGxOjo6tHnzZvn9fmVnZ6u6ulrTp0+XJPn9/qA9R5577jldunRJDz30kB566KGB9nvuuUcvvfTS1b8DAAAQ0xzvM2ID+4wAABB7Rvr9zW/TAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArEqwXQAAYOR6+4yONl1QW1e3Jqe4le9LU3ycy3ZZwFUhjABAjKhp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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAioAAAGdCAYAAAA8F1jjAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAAArFUlEQVR4nO3df1Tc1Z3/8dcAgcFsGJukgdEgYqr5Rf3BsEGgqWeNwWTddLM9rlg3iXGTXbFaxazuCSe7Ijmesrb+bgNNNNGNiSnHX605jan84Q8ibbMh5JxGrLGGLhgHWcg6YG3ADPf7Rxa+jkDKZxjgzszzcc7nj7ncz8z7noHMK/d+PndcxhgjAAAACyVMdgEAAAAjIagAAABrEVQAAIC1CCoAAMBaBBUAAGAtggoAALAWQQUAAFiLoAIAAKyVNNkFjEZ/f78++ugjTZs2TS6Xa7LLAQAAo2CMUU9Pj8477zwlJIQ3NxIVQeWjjz5SZmbmZJcBAADC0NbWptmzZ4d1blQElWnTpkk6M9C0tLRJrgYAAIxGd3e3MjMzBz/HwxEVQWVguSctLY2gAgBAlBnLZRtcTAsAAKxFUAEAANYiqAAAAGsRVAAAgLUIKgAAwFoEFQAAYC2CCgAAsBZBBQAAWCsqNnwDACCeBfuNDracVEfPKc2a5tai7OlKTIiP774jqAAAYLH9R/2q3Nssf+DUYJvX41bFigValuOdxMomBks/AABYav9Rv27bdTgkpEhSe+CUbtt1WPuP+iepsolDUAEAwELBfqPKvc0yw/xsoK1yb7OC/cP1iB0EFQAALHSw5eSQmZQvMpL8gVM62HJy4oqaBAQVAAAs1NEzckgJp1+0IqgAAGChWdPcEe0XrQgqAABYaFH2dHk9bo10E7JLZ+7+WZQ9fSLLmnAEFQAALJSY4FLFigWSNCSsDDyuWLEg5vdTCSuoVFdXKzs7W263Wz6fT/X19Wftv2XLFs2fP1+pqamaO3eudu7cGVaxmBjBfqNffdClnx85oV990BXzV5QDgK2W5XhVsypXGZ7Q5Z0Mj1s1q3LjYh8Vxxu+1dbWqqysTNXV1SoqKtLWrVu1fPlyNTc364ILLhjSv6amRuXl5XryySf1l3/5lzp48KD+6Z/+SV/5yle0YsWKiAwCkRPvGwsBgG2W5Xi1dEFG3O5M6zLGOPrvcn5+vnJzc1VTUzPYNn/+fK1cuVJVVVVD+hcWFqqoqEg//OEPB9vKysp06NAhHThwYFSv2d3dLY/Ho0AgoLS0NCflwoGBjYW+/Asx8KcQL+kdABAZkfj8drT009fXp8bGRhUXF4e0FxcXq6GhYdhzent75XaHTlmlpqbq4MGD+vzzz0c8p7u7O+TA+GJjIQCAjRwFlc7OTgWDQaWnp4e0p6enq729fdhzrr32Wj311FNqbGyUMUaHDh3Sjh079Pnnn6uzs3PYc6qqquTxeAaPzMxMJ2UiDGwsBACwUVgX07pcoetixpghbQP+/d//XcuXL9eVV16pKVOm6G//9m+1du1aSVJiYuKw55SXlysQCAwebW1t4ZQJB9hYCABgI0dBZebMmUpMTBwye9LR0TFklmVAamqqduzYoc8++0x/+MMf1NraqgsvvFDTpk3TzJkzhz0nJSVFaWlpIQfGFxsLAQBs5CioJCcny+fzqa6uLqS9rq5OhYWFZz13ypQpmj17thITE/XTn/5Uf/M3f6OEBLZxsQUbCwEAbOQ4KWzYsEFPPfWUduzYoXfffVd33323WltbVVpaKunMss2aNWsG+x87dky7du3S+++/r4MHD+rGG2/U0aNH9f3vfz9yo8CYsbEQAMBGjvdRKSkpUVdXlzZv3iy/36+cnBzt27dPWVlZkiS/36/W1tbB/sFgUA8//LDee+89TZkyRX/1V3+lhoYGXXjhhREbBCJjYGOhL++jksE+KgCASeJ4H5XJwD4qEyvYb+J2YyEAQORE4vPb8YwKYl9igksFc2ZMdhkAAPClhAAAwF4EFQAAYC2CCgAAsBZBBQAAWIuLaQEAUYE7EuMTQQUAYL39R/1D9njyssdTXGDpBwBgtf1H/bpt1+Eh3/DeHjil23Yd1v6j/kmqDBOBoAIAsFaw36hyb7OG25l0oK1yb7OC/dbvXYowEVQAANY62HJyyEzKFxlJ/sApHWw5OXFFYUIRVAAA1uroGTmkhNMP0YegAgCw1qxp7oj2Q/QhqAAArLUoe7q8HrdGugnZpTN3/yzKnj6RZWECEVQAANZKTHCpYsUCSRoSVgYeV6xYwH4qMYygAgCw2rIcr2pW5SrDE7q8k+Fxq2ZVLvuoxDg2fAMAWG9ZjldLF2SwM20citugwlbMABBdEhNcKpgzY7LLwASLy6DCVswAAESHuLtGha2YAQCIHnEVVNiKGQCA6BJXQYWtmAEAiC5xFVTYihkAgOgSV0GFrZgBAIgucRVU2IoZAIDoEldBha2YAQCILnEVVCS2YgYAIJrE5YZvbMUMAEB0iMugIrEVMwAA0SDuln4AAED0IKgAAABrEVQAAIC1CCoAAMBaBBUAAGAtggoAALBWWEGlurpa2dnZcrvd8vl8qq+vP2v/3bt367LLLtM555wjr9erW265RV1dXWEVDAAA4ofjoFJbW6uysjJt2rRJTU1NWrx4sZYvX67W1tZh+x84cEBr1qzRunXr9M477+j555/Xf/3Xf2n9+vVjLh4AAMQ2x0HlkUce0bp167R+/XrNnz9fjz32mDIzM1VTUzNs/1//+te68MILdeeddyo7O1vf+MY3dOutt+rQoUNjLh4AAMQ2R0Glr69PjY2NKi4uDmkvLi5WQ0PDsOcUFhbqww8/1L59+2SM0ccff6wXXnhB11133Yiv09vbq+7u7pADAADEH0dBpbOzU8FgUOnp6SHt6enpam9vH/acwsJC7d69WyUlJUpOTlZGRobOPfdc/ehHPxrxdaqqquTxeAaPzMxMJ2UCAIAYEdbFtC5X6Jf3GWOGtA1obm7WnXfeqfvuu0+NjY3av3+/WlpaVFpaOuLzl5eXKxAIDB5tbW3hlAkAAKKcoy8lnDlzphITE4fMnnR0dAyZZRlQVVWloqIi3XvvvZKkSy+9VFOnTtXixYv1wAMPyOv1DjknJSVFKSkpTkoDAAAxyNGMSnJysnw+n+rq6kLa6+rqVFhYOOw5n332mRISQl8mMTFR0pmZGAAAgJE4XvrZsGGDnnrqKe3YsUPvvvuu7r77brW2tg4u5ZSXl2vNmjWD/VesWKGXXnpJNTU1On78uN5++23deeedWrRokc4777zIjQQAAMQcR0s/klRSUqKuri5t3rxZfr9fOTk52rdvn7KysiRJfr8/ZE+VtWvXqqenRz/+8Y/1L//yLzr33HN19dVX68EHH4zcKAAAQExymShYf+nu7pbH41EgEFBaWtpklwMAAEYhEp/ffNcPAACwFkEFAABYi6ACAACsRVABAADWIqgAAABrEVQAAIC1CCoAAMBaBBUAAGAtggoAALAWQQUAAFiLoAIAAKxFUAEAANYiqAAAAGsRVAAAgLUIKgAAwFoEFQAAYC2CCgAAsBZBBQAAWIugAgAArEVQAQAA1iKoAAAAaxFUAACAtQgqAADAWgQVAABgLYIKAACwFkEFAABYi6ACAACsRVABAADWIqgAAABrEVQAAIC1CCoAAMBaBBUAAGAtggoAALAWQQUAAFgrrKBSXV2t7Oxsud1u+Xw+1dfXj9h37dq1crlcQ46FCxeGXTQAAIgPjoNKbW2tysrKtGnTJjU1NWnx4sVavny5Wltbh+3/+OOPy+/3Dx5tbW2aPn26/v7v/37MxQMAgNjmMsYYJyfk5+crNzdXNTU1g23z58/XypUrVVVV9WfP/9nPfqZvf/vbamlpUVZW1qhes7u7Wx6PR4FAQGlpaU7KBQAAkyQSn9+OZlT6+vrU2Nio4uLikPbi4mI1NDSM6jm2b9+ua6655qwhpbe3V93d3SEHAACIP46CSmdnp4LBoNLT00Pa09PT1d7e/mfP9/v9evXVV7V+/fqz9quqqpLH4xk8MjMznZQJAABiRFgX07pcrpDHxpghbcN55plndO6552rlypVn7VdeXq5AIDB4tLW1hVMmAACIcklOOs+cOVOJiYlDZk86OjqGzLJ8mTFGO3bs0OrVq5WcnHzWvikpKUpJSXFSGgAAiEGOZlSSk5Pl8/lUV1cX0l5XV6fCwsKznvvmm2/q97//vdatW+e8SgAAEJcczahI0oYNG7R69Wrl5eWpoKBA27ZtU2trq0pLSyWdWbY5ceKEdu7cGXLe9u3blZ+fr5ycnMhUDgAAYp7joFJSUqKuri5t3rxZfr9fOTk52rdv3+BdPH6/f8ieKoFAQC+++KIef/zxyFQNAADiguN9VCYD+6gAABB9JnwfFQAAgIlEUAEAANYiqAAAAGsRVAAAgLUIKgAAwFoEFQAAYC2CCgAAsBZBBQAAWIugAgAArEVQAQAA1iKoAAAAaxFUAACAtQgqAADAWgQVAABgLYIKAACwFkEFAABYi6ACAACsRVABAADWIqgAAABrEVQAAIC1CCoAAMBaBBUAAGAtggoAALAWQQUAAFiLoAIAAKxFUAEAANYiqAAAAGsRVAAAgLWSJrsAYDIF+40OtpxUR88pzZrm1qLs6UpMcE12WQCA/0NQQdzaf9Svyr3N8gdODbZ5PW5VrFigZTneSawMADCApR/Epf1H/bpt1+GQkCJJ7YFTum3XYe0/6p+kygAAX0RQQdwJ9htV7m2WGeZnA22Ve5sV7B+uBwBgIhFUEHcOtpwcMpPyRUaSP3BKB1tOTlxRAIBhEVQQdzp6Rg4p4fQDAIyfsIJKdXW1srOz5Xa75fP5VF9ff9b+vb292rRpk7KyspSSkqI5c+Zox44dYRUMjNWsae6I9gMAjB/Hd/3U1taqrKxM1dXVKioq0tatW7V8+XI1NzfrggsuGPacG264QR9//LG2b9+ur33ta+ro6NDp06fHXDwQjkXZ0+X1uNUeODXsdSouSRmeM7cqAwAml8sY4+iKwfz8fOXm5qqmpmawbf78+Vq5cqWqqqqG9N+/f79uvPFGHT9+XNOnh/cPf3d3tzwejwKBgNLS0sJ6DuCLBu76kRQSVgZ2UKlZlcstygAwRpH4/Ha09NPX16fGxkYVFxeHtBcXF6uhoWHYc1555RXl5eXpBz/4gc4//3xdcskluueee/SnP/0prIKBSFiW41XNqlxleEKXdzI8bkIKAFjE0dJPZ2engsGg0tPTQ9rT09PV3t4+7DnHjx/XgQMH5Ha79fLLL6uzs1Pf/e53dfLkyRGvU+nt7VVvb+/g4+7ubidlAqOyLMerpQsy2JkWACwW1s60LlfoP+TGmCFtA/r7++VyubR79255PB5J0iOPPKLrr79eW7ZsUWpq6pBzqqqqVFlZGU5pgCOJCS4VzJkx2WUAAEbgaOln5syZSkxMHDJ70tHRMWSWZYDX69X5558/GFKkM9e0GGP04YcfDntOeXm5AoHA4NHW1uakTAAAECMcBZXk5GT5fD7V1dWFtNfV1amwsHDYc4qKivTRRx/p008/HWw7duyYEhISNHv27GHPSUlJUVpaWsgBAADij+N9VDZs2KCnnnpKO3bs0Lvvvqu7775bra2tKi0tlXRmNmTNmjWD/W+66SbNmDFDt9xyi5qbm/XWW2/p3nvv1T/+4z8Ou+wDAAAwwPE1KiUlJerq6tLmzZvl9/uVk5Ojffv2KSsrS5Lk9/vV2to62P8v/uIvVFdXp+9973vKy8vTjBkzdMMNN+iBBx6I3CgAAEBMcryPymRgHxUAAKLPhO+jAgAAMJEIKgAAwFoEFQAAYC2CCgAAsBZBBQAAWIugAgAArEVQAQAA1iKoAAAAaxFUAACAtQgqAADAWgQVAABgLYIKAACwFkEFAABYi6ACAACsRVABAADWIqgAAABrEVQAAIC1CCoAAMBaBBUAAGAtggoAALAWQQUAAFiLoAIAAKxFUAEAANYiqAAAAGsRVAAAgLUIKgAAwFoEFQAAYC2CCgAAsBZBBQAAWIugAgAArEVQAQAA1iKoAAAAaxFUAACAtQgqAADAWgQVAABgrbCCSnV1tbKzs+V2u+Xz+VRfXz9i3zfeeEMul2vI8bvf/S7sogEAQHxwHFRqa2tVVlamTZs2qampSYsXL9by5cvV2tp61vPee+89+f3+wePiiy8Ou2gAABAfHAeVRx55ROvWrdP69es1f/58PfbYY8rMzFRNTc1Zz5s1a5YyMjIGj8TExLCLBgAA8cFRUOnr61NjY6OKi4tD2ouLi9XQ0HDWc6+44gp5vV4tWbJEr7/++ln79vb2qru7O+QAAADxx1FQ6ezsVDAYVHp6ekh7enq62tvbhz3H6/Vq27ZtevHFF/XSSy9p7ty5WrJkid56660RX6eqqkoej2fwyMzMdFImAACIEUnhnORyuUIeG2OGtA2YO3eu5s6dO/i4oKBAbW1teuihh/TNb35z2HPKy8u1YcOGwcfd3d2EFQAA4pCjGZWZM2cqMTFxyOxJR0fHkFmWs7nyyiv1/vvvj/jzlJQUpaWlhRwAACD+OAoqycnJ8vl8qqurC2mvq6tTYWHhqJ+nqalJXq/XyUsDAIA45HjpZ8OGDVq9erXy8vJUUFCgbdu2qbW1VaWlpZLOLNucOHFCO3fulCQ99thjuvDCC7Vw4UL19fVp165devHFF/Xiiy9GdiQAACDmOA4qJSUl6urq0ubNm+X3+5WTk6N9+/YpKytLkuT3+0P2VOnr69M999yjEydOKDU1VQsXLtQvfvEL/fVf/3XkRgEAAGKSyxhjJruIP6e7u1sej0eBQIDrVQAAiBKR+Pzmu34AAIC1CCoAAMBaBBUAAGAtggoAALAWQQUAAFiLoAIAAKxFUAEAANYiqAAAAGsRVAAAgLUIKgAAwFoEFQAAYC2CCgAAsBZBBQAAWIugAgAArEVQAQAA1iKoAAAAaxFUAACAtQgqAADAWgQVAABgLYIKAACwFkEFAABYi6ACAACsRVABAADWIqgAAABrEVQAAIC1CCoAAMBaBBUAAGAtggoAALAWQQUAAFiLoAIAAKxFUAEAANYiqAAAAGsRVAAAgLUIKgAAwFphBZXq6mplZ2fL7XbL5/Opvr5+VOe9/fbbSkpK0uWXXx7OywIAgDjjOKjU1taqrKxMmzZtUlNTkxYvXqzly5ertbX1rOcFAgGtWbNGS5YsCbtYAAAQX1zGGOPkhPz8fOXm5qqmpmawbf78+Vq5cqWqqqpGPO/GG2/UxRdfrMTERP3sZz/TkSNHRv2a3d3d8ng8CgQCSktLc1IuAACYJJH4/HY0o9LX16fGxkYVFxeHtBcXF6uhoWHE855++ml98MEHqqioGNXr9Pb2qru7O+QAAADxx1FQ6ezsVDAYVHp6ekh7enq62tvbhz3n/fff18aNG7V7924lJSWN6nWqqqrk8XgGj8zMTCdlAgCAGBHWxbQulyvksTFmSJskBYNB3XTTTaqsrNQll1wy6ucvLy9XIBAYPNra2sIpEwAARLnRTXH8n5kzZyoxMXHI7ElHR8eQWRZJ6unp0aFDh9TU1KQ77rhDktTf3y9jjJKSkvTaa6/p6quvHnJeSkqKUlJSnJQGAABikKMZleTkZPl8PtXV1YW019XVqbCwcEj/tLQ0/fa3v9WRI0cGj9LSUs2dO1dHjhxRfn7+2KoHAAAxzdGMiiRt2LBBq1evVl5engoKCrRt2za1traqtLRU0pllmxMnTmjnzp1KSEhQTk5OyPmzZs2S2+0e0g4AAPBljoNKSUmJurq6tHnzZvn9fuXk5Gjfvn3KysqSJPn9/j+7pwoAAMBoON5HZTKwjwoAANFnwvdRAQAAmEgEFQAAYC2CCgAAsBZBBQAAWIugAgAArOX49mTEjmC/0cGWk+roOaVZ09xalD1diQlDvwoBAIDJQlCJU/uP+lW5t1n+wKnBNq/HrYoVC7QsxzuJlQEA8P+x9BOH9h/167Zdh0NCiiS1B07ptl2Htf+of5IqAwAgFEElzgT7jSr3Nmu4Xf4G2ir3NivYb/0+gACAOEBQiTMHW04OmUn5IiPJHzilgy0nJ64oAABGQFCJMx09I4eUcPoBADCeCCpxZtY0d0T7AQAwnrjrJ84syp4ur8et9sCpYa9TcUnK8Jy5VRkAENuiYZsKgkqcSUxwqWLFAt2267BcUkhYGfjVrFixwLpfVABAZEXLNhUs/cShZTle1azKVYYndHknw+NWzapcq35BAQCRF03bVDCjEqeW5Xi1dEGG9VN+AIDI+nPbVLh0ZpuKpQsyrPhMIKjEscQElwrmzJjsMgAAE8jJNhU2fEaw9AMAQByJtm0qCCoAAMSRaNumgqACAEAcGdimYqSrT1w6c/ePLdtUEFQAAIgjA9tUSBoSVmzcpoKgAgBAnImmbSq46wcAgDgULdtUEFQAAIhT0bBNBUs/AADAWgQVAABgLYIKAACwFkEFAABYi6ACAACsRVABAADWIqgAAABrEVQAAIC1CCoAAMBaBBUAAGCtsIJKdXW1srOz5Xa75fP5VF9fP2LfAwcOqKioSDNmzFBqaqrmzZunRx99NOyCAQBA/HD8XT+1tbUqKytTdXW1ioqKtHXrVi1fvlzNzc264IILhvSfOnWq7rjjDl166aWaOnWqDhw4oFtvvVVTp07VP//zP0dkEAAAIDa5jDHGyQn5+fnKzc1VTU3NYNv8+fO1cuVKVVVVjeo5vv3tb2vq1Kl69tlnR9W/u7tbHo9HgUBAaWlpTsoFYkKw31j/DacA8GWR+Px2NKPS19enxsZGbdy4MaS9uLhYDQ0No3qOpqYmNTQ06IEHHhixT29vr3p7ewcfd3d3OykTiCn7j/pVubdZ/sCpwTavx62KFQu0LMc7iZUBwPhzdI1KZ2engsGg0tPTQ9rT09PV3t5+1nNnz56tlJQU5eXl6fbbb9f69etH7FtVVSWPxzN4ZGZmOikTiBn7j/p1267DISFFktoDp3TbrsPaf9Q/SZUBwMQI62Jalyt0ytkYM6Tty+rr63Xo0CH95Cc/0WOPPaY9e/aM2Le8vFyBQGDwaGtrC6dMIKoF+40q9zZruLXZgbbKvc0K9jtavQWAqOJo6WfmzJlKTEwcMnvS0dExZJbly7KzsyVJX//61/Xxxx/r/vvv13e+851h+6akpCglJcVJaUDMOdhycshMyhcZSf7AKR1sOamCOTMmrjAAmECOZlSSk5Pl8/lUV1cX0l5XV6fCwsJRP48xJuQaFABDdfSMHFLC6QcA0cjx7ckbNmzQ6tWrlZeXp4KCAm3btk2tra0qLS2VdGbZ5sSJE9q5c6ckacuWLbrgggs0b948SWf2VXnooYf0ve99L4LDAGLPrGnuiPYDgGjkOKiUlJSoq6tLmzdvlt/vV05Ojvbt26esrCxJkt/vV2tr62D//v5+lZeXq6WlRUlJSZozZ47+4z/+Q7feemvkRgHEoEXZ0+X1uNUeODXsdSouSRmeM7cqA0CscryPymRgHxXEq4G7fiSFhJWBS9drVuVyizIAa0Xi85vv+gEstizHq5pVucrwhC7vZHjchBQAccHx0g+AibUsx6ulCzLYmRZAXCKoAFEgMcHFLcgA4hJLPwAAwFoEFQAAYC2CCgAAsBZBBQAAWIugAgAArEVQAQAA1iKoAAAAaxFUAACAtQgqAADAWgQVAABgLYIKAACwFkEFAABYi6ACAACsRVABAADWIqgAAABrEVQAAIC1CCoAAMBaBBUAAGAtggoAALAWQQUAAFiLoAIAAKxFUAEAANZKmuwCAACIlGC/0cGWk+roOaVZ09xalD1diQmuyS4LY0BQAQDEhP1H/arc2yx/4NRgm9fjVsWKBVqW453EyjAWLP0AAKLe/qN+3bbrcEhIkaT2wCndtuuw9h/1T1JlGCuCCgAgqgX7jSr3NssM87OBtsq9zQr2D9cDtiOoAACi2sGWk0NmUr7ISPIHTulgy8mJKwoRQ1ABAES1jp6RQ0o4/WAXggoAIKrNmuaOaD/YhaACAIhqi7Kny+txa6SbkF06c/fPouzpE1kWIoSgAgCIaokJLlWsWCBJQ8LKwOOKFQvYTyVKhRVUqqurlZ2dLbfbLZ/Pp/r6+hH7vvTSS1q6dKm++tWvKi0tTQUFBfrlL38ZdsEAAHzZshyvalblKsMTuryT4XGrZlUu+6hEMccbvtXW1qqsrEzV1dUqKirS1q1btXz5cjU3N+uCCy4Y0v+tt97S0qVL9f3vf1/nnnuunn76aa1YsUK/+c1vdMUVV0RkEAAALMvxaumCDHamjTEuY4yjG8vz8/OVm5urmpqawbb58+dr5cqVqqqqGtVzLFy4UCUlJbrvvvtG1b+7u1sej0eBQEBpaWlOygUAAJMkEp/fjpZ++vr61NjYqOLi4pD24uJiNTQ0jOo5+vv71dPTo+nTR76oqbe3V93d3SEHAACIP46CSmdnp4LBoNLT00Pa09PT1d7ePqrnePjhh/XHP/5RN9xww4h9qqqq5PF4Bo/MzEwnZQIAgBgR1sW0Llfoep8xZkjbcPbs2aP7779ftbW1mjVr1oj9ysvLFQgEBo+2trZwygQAAFHO0cW0M2fOVGJi4pDZk46OjiGzLF9WW1urdevW6fnnn9c111xz1r4pKSlKSUlxUhoAAIhBjmZUkpOT5fP5VFdXF9JeV1enwsLCEc/bs2eP1q5dq+eee07XXXddeJUCAIC44/j25A0bNmj16tXKy8tTQUGBtm3bptbWVpWWlko6s2xz4sQJ7dy5U9KZkLJmzRo9/vjjuvLKKwdnY1JTU+XxeCI4FAAAEGscB5WSkhJ1dXVp8+bN8vv9ysnJ0b59+5SVlSVJ8vv9am1tHey/detWnT59Wrfffrtuv/32wfabb75ZzzzzzNhHAAAAYpbjfVQmA/uoAAAQfSLx+e14RgUAYJdgv2E3VsQsggoARLH9R/2q3Nssf+DUYJvX41bFigV8vw1iAt+eDABRav9Rv27bdTgkpEhSe+CUbtt1WPuP+iepMiByCCoAEIWC/UaVe5s13EWGA22Ve5sV7Lf+MkTgrAgqABCFDracHDKT8kVGkj9wSgdbTk5cUcA4IKgAQBTq6Bk5pITTD7AVQQUAotCsae6I9gNsRVABgCi0KHu6vB63RroJ2aUzd/8syp4+kWUBEUdQAYAolJjgUsWKBZI0JKwMPK5YsYD9VBD1CCoAEKWW5XhVsypXGZ7Q5Z0Mj1s1q3LZRwUxgQ3fACCKLcvxaumCDHamRcwiqABAlEtMcKlgzozJLgMYFyz9AAAAaxFUAACAtQgqAADAWgQVAABgLYIKAACwFkEFAABYi6ACAACsRVABAADWIqgAAABrRcXOtMYYSVJ3d/ckVwIAAEZr4HN74HM8HFERVHp6eiRJmZmZk1wJAABwqqenRx6PJ6xzXWYsMWeC9Pf366OPPtK0adPkco3ti7a6u7uVmZmptrY2paWlRahCOzHW2MRYYxNjjU3xPlZjjHp6enTeeecpISG8q02iYkYlISFBs2fPjuhzpqWlxfwvzQDGGpsYa2xirLEpnsca7kzKAC6mBQAA1iKoAAAAa8VdUElJSVFFRYVSUlImu5Rxx1hjE2ONTYw1NjHWsYuKi2kBAEB8irsZFQAAED0IKgAAwFoEFQAAYC2CCgAAsFbMBZXq6mplZ2fL7XbL5/Opvr7+rP3ffPNN+Xw+ud1uXXTRRfrJT34yQZVGhpPx+v1+3XTTTZo7d64SEhJUVlY2cYVGgJOxvvTSS1q6dKm++tWvKi0tTQUFBfrlL385gdWOjZOxHjhwQEVFRZoxY4ZSU1M1b948PfrooxNY7dg4/Zsd8PbbbyspKUmXX375+BYYQU7G+sYbb8jlcg05fve7301gxeFz+r729vZq06ZNysrKUkpKiubMmaMdO3ZMULVj42Ssa9euHfZ9Xbhw4QRWHD6n7+vu3bt12WWX6ZxzzpHX69Utt9yirq4uZy9qYshPf/pTM2XKFPPkk0+a5uZmc9ddd5mpU6ea//7v/x62//Hjx80555xj7rrrLtPc3GyefPJJM2XKFPPCCy9McOXhcTrelpYWc+edd5r//M//NJdffrm56667JrbgMXA61rvuuss8+OCD5uDBg+bYsWOmvLzcTJkyxRw+fHiCK3fO6VgPHz5snnvuOXP06FHT0tJinn32WXPOOeeYrVu3TnDlzjkd64BPPvnEXHTRRaa4uNhcdtllE1PsGDkd6+uvv24kmffee8/4/f7B4/Tp0xNcuXPhvK/f+ta3TH5+vqmrqzMtLS3mN7/5jXn77bcnsOrwOB3rJ598EvJ+trW1menTp5uKioqJLTwMTsdaX19vEhISzOOPP26OHz9u6uvrzcKFC83KlSsdvW5MBZVFixaZ0tLSkLZ58+aZjRs3Dtv/X//1X828efNC2m699VZz5ZVXjluNkeR0vF901VVXRVVQGctYByxYsMBUVlZGurSIi8RY/+7v/s6sWrUq0qVFXLhjLSkpMf/2b/9mKioqoiaoOB3rQFD53//93wmoLrKcjvXVV181Ho/HdHV1TUR5ETXWv9eXX37ZuFwu84c//GE8yosop2P94Q9/aC666KKQtieeeMLMnj3b0evGzNJPX1+fGhsbVVxcHNJeXFyshoaGYc/51a9+NaT/tddeq0OHDunzzz8ft1ojIZzxRqtIjLW/v189PT2aPn36eJQYMZEYa1NTkxoaGnTVVVeNR4kRE+5Yn376aX3wwQeqqKgY7xIjZizv6xVXXCGv16slS5bo9ddfH88yIyKcsb7yyivKy8vTD37wA51//vm65JJLdM899+hPf/rTRJQctkj8vW7fvl3XXHONsrKyxqPEiAlnrIWFhfrwww+1b98+GWP08ccf64UXXtB1113n6LWj4ksJR6Ozs1PBYFDp6ekh7enp6Wpvbx/2nPb29mH7nz59Wp2dnfJ6veNW71iFM95oFYmxPvzww/rjH/+oG264YTxKjJixjHX27Nn6n//5H50+fVr333+/1q9fP56ljlk4Y33//fe1ceNG1dfXKykpev75CmesXq9X27Ztk8/nU29vr5599lktWbJEb7zxhr75zW9ORNlhCWesx48f14EDB+R2u/Xyyy+rs7NT3/3ud3Xy5Emrr1MZ679Nfr9fr776qp577rnxKjFiwhlrYWGhdu/erZKSEp06dUqnT5/Wt771Lf3oRz9y9NrR85c+Si6XK+SxMWZI25/rP1y7rZyON5qFO9Y9e/bo/vvv189//nPNmjVrvMqLqHDGWl9fr08//VS//vWvtXHjRn3ta1/Td77znfEsMyJGO9ZgMKibbrpJlZWVuuSSSyaqvIhy8r7OnTtXc+fOHXxcUFCgtrY2PfTQQ1YHlQFOxtrf3y+Xy6Xdu3cPftPuI488ouuvv15btmxRamrquNc7FuH+2/TMM8/o3HPP1cqVK8epsshzMtbm5mbdeeeduu+++3TttdfK7/fr3nvvVWlpqbZv3z7q14yZoDJz5kwlJiYOSXYdHR1DEuCAjIyMYfsnJSVpxowZ41ZrJIQz3mg1lrHW1tZq3bp1ev7553XNNdeMZ5kRMZaxZmdnS5K+/vWv6+OPP9b9999vdVBxOtaenh4dOnRITU1NuuOOOySd+YAzxigpKUmvvfaarr766gmp3alI/b1eeeWV2rVrV6TLi6hwxur1enX++ecPhhRJmj9/vowx+vDDD3XxxRePa83hGsv7aozRjh07tHr1aiUnJ49nmRERzlirqqpUVFSke++9V5J06aWXaurUqVq8eLEeeOCBUa9axMw1KsnJyfL5fKqrqwtpr6urU2Fh4bDnFBQUDOn/2muvKS8vT1OmTBm3WiMhnPFGq3DHumfPHq1du1bPPfec4zXRyRKp99UYo97e3kiXF1FOx5qWlqbf/va3OnLkyOBRWlqquXPn6siRI8rPz5+o0h2L1Pva1NRk9ZK0FN5Yi4qK9NFHH+nTTz8dbDt27JgSEhI0e/bsca13LMbyvr755pv6/e9/r3Xr1o1niRETzlg/++wzJSSExozExERJ/3/1YlQcXXpruYFbp7Zv326am5tNWVmZmTp16uDV1Bs3bjSrV68e7D9we/Ldd99tmpubzfbt26Py9uTRjtcYY5qamkxTU5Px+XzmpptuMk1NTeadd96ZjPIdcTrW5557ziQlJZktW7aE3Ar4ySefTNYQRs3pWH/84x+bV155xRw7dswcO3bM7Nixw6SlpZlNmzZN1hBGLZzf4S+Kprt+nI710UcfNS+//LI5duyYOXr0qNm4caORZF588cXJGsKoOR1rT0+PmT17trn++uvNO++8Y958801z8cUXm/Xr10/WEEYt3N/hVatWmfz8/Ikud0ycjvXpp582SUlJprq62nzwwQfmwIEDJi8vzyxatMjR68ZUUDHGmC1btpisrCyTnJxscnNzzZtvvjn4s5tvvtlcddVVIf3feOMNc8UVV5jk5GRz4YUXmpqamgmueGycjlfSkCMrK2tiiw6Tk7FeddVVw4715ptvnvjCw+BkrE888YRZuHChOeecc0xaWpq54oorTHV1tQkGg5NQuXNOf4e/KJqCijHOxvrggw+aOXPmGLfbbb7yla+Yb3zjG+YXv/jFJFQdHqfv67vvvmuuueYak5qaambPnm02bNhgPvvsswmuOjxOx/rJJ5+Y1NRUs23btgmudOycjvWJJ54wCxYsMKmpqcbr9Zp/+Id/MB9++KGj13QZ42T+BQAAYOLEzDUqAAAg9hBUAACAtQgqAADAWgQVAABgLYIKAACwFkEFAABYi6ACAACsRVABAADWIqgAAABrEVQAAIC1CCoAAMBaBBUAAGCt/wdxUOz/6a0clAAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -1231,7 +1231,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 29, @@ -1262,7 +1262,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "67e245355ef74e23999a2b46f5f4cbaa", + "model_id": "5ee2f89cadea46a8926434ab39f44805", "version_major": 2, "version_minor": 0 }, @@ -1275,7 +1275,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -1289,7 +1289,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 30, @@ -1541,7 +1541,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 31, @@ -1583,7 +1583,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -1755,159 +1755,159 @@ "clusterphase_preference\n", "\n", "phase_preference: Workflow\n", + "\n", + "clusterphase_preferenceInputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", "\n", "clusterphase_preferenceOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Outputs\n", "\n", "\n", "clusterphase_preferenceelement\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "element: UserInput\n", "\n", "\n", "clusterphase_preferenceelementInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Inputs\n", "\n", "\n", "clusterphase_preferenceelementOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Outputs\n", "\n", + "\n", + "clusterphase_preferencemin_phase1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "min_phase1: LammpsMinimize\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Inputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", + "\n", + "clusterphase_preferencemin_phase1Outputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Outputs\n", + "\n", "\n", "clusterphase_preferencemin_phase2\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "min_phase2: LammpsMinimize\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Inputs\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Outputs\n", "\n", "\n", "clusterphase_preferencecompare\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "compare: PerAtomEnergyDifference\n", "\n", - "\n", - "clusterphase_preferencecompareOutputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Outputs\n", - "\n", "\n", "clusterphase_preferencecompareInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Inputs\n", "\n", - "\n", - "clusterphase_preferenceInputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Inputs\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "min_phase1: LammpsMinimize\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Outputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Outputs\n", - "\n", - "\n", - "clusterphase_preferencemin_phase1Inputs\n", + "\n", + "clusterphase_preferencecompareOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Outputs\n", "\n", "\n", "\n", @@ -2134,192 +2134,192 @@ "\n", "\n", "clusterphase_preferenceInputsphase2\n", - "\n", - "phase2\n", + "\n", + "phase2\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputscrystalstructure\n", - "\n", - "crystalstructure\n", + "\n", + "crystalstructure\n", "\n", "\n", "\n", "clusterphase_preferenceInputsphase2->clusterphase_preferencemin_phase2Inputscrystalstructure\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputslattice_guess2\n", - "\n", - "lattice_guess2\n", + "\n", + "lattice_guess2\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputslattice_guess\n", - "\n", - "lattice_guess\n", + "\n", + "lattice_guess\n", "\n", "\n", "\n", "clusterphase_preferenceInputslattice_guess2->clusterphase_preferencemin_phase2Inputslattice_guess\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__c\n", - "\n", - "min_phase2__structure__c\n", + "\n", + "min_phase2__structure__c\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputsstructure__c\n", - "\n", - "structure__c\n", + "\n", + "structure__c\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__c->clusterphase_preferencemin_phase2Inputsstructure__c\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__covera\n", - "\n", - "min_phase2__structure__covera\n", + "\n", + "min_phase2__structure__covera\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputsstructure__covera\n", - "\n", - "structure__covera\n", + "\n", + "structure__covera\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__covera->clusterphase_preferencemin_phase2Inputsstructure__covera\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__u\n", - "\n", - "min_phase2__structure__u\n", + "\n", + "min_phase2__structure__u\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputsstructure__u\n", - "\n", - "structure__u\n", + "\n", + "structure__u\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__u->clusterphase_preferencemin_phase2Inputsstructure__u\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__orthorhombic\n", - "\n", - "min_phase2__structure__orthorhombic\n", + "\n", + "min_phase2__structure__orthorhombic\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputsstructure__orthorhombic\n", - "\n", - "structure__orthorhombic\n", + "\n", + "structure__orthorhombic\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__orthorhombic->clusterphase_preferencemin_phase2Inputsstructure__orthorhombic\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__cubic\n", - "\n", - "min_phase2__structure__cubic\n", + "\n", + "min_phase2__structure__cubic\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputsstructure__cubic\n", - "\n", - "structure__cubic\n", + "\n", + "structure__cubic\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__structure__cubic->clusterphase_preferencemin_phase2Inputsstructure__cubic\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__calc__n_ionic_steps\n", - "\n", - "min_phase2__calc__n_ionic_steps: int\n", + "\n", + "min_phase2__calc__n_ionic_steps: int\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputscalc__n_ionic_steps\n", - "\n", - "calc__n_ionic_steps: int\n", + "\n", + "calc__n_ionic_steps: int\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__calc__n_ionic_steps->clusterphase_preferencemin_phase2Inputscalc__n_ionic_steps\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__calc__n_print\n", - "\n", - "min_phase2__calc__n_print: int\n", + "\n", + "min_phase2__calc__n_print: int\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputscalc__n_print\n", - "\n", - "calc__n_print: int\n", + "\n", + "calc__n_print: int\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__calc__n_print->clusterphase_preferencemin_phase2Inputscalc__n_print\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__calc__pressure\n", - "\n", - "min_phase2__calc__pressure\n", + "\n", + "min_phase2__calc__pressure\n", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Inputscalc__pressure\n", - "\n", - "calc__pressure\n", + "\n", + "calc__pressure\n", "\n", "\n", "\n", "clusterphase_preferenceInputsmin_phase2__calc__pressure->clusterphase_preferencemin_phase2Inputscalc__pressure\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -2960,7 +2960,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 37, @@ -3003,7 +3003,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -3044,11 +3044,11 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel job was not connected to job, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel job was not connected to job, andthus could not disconnect from it.\n", " warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel energy_pot was not connected to energy1, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel energy_pot was not connected to energy1, andthus could not disconnect from it.\n", " warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n" ] } @@ -3099,7 +3099,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -3240,9 +3240,9 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to true, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to true, andthus could not disconnect from it.\n", " warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:110: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n" ] } @@ -3323,11 +3323,21 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.569 > 0.2\n", - "0.648 > 0.2\n", - "0.977 > 0.2\n", - "0.148 <= 0.2\n", - "Finally 0.148\n" + "0.885 > 0.2\n", + "0.790 > 0.2\n", + "0.395 > 0.2\n", + "0.593 > 0.2\n", + "0.220 > 0.2\n", + "0.440 > 0.2\n", + "0.523 > 0.2\n", + "0.407 > 0.2\n", + "0.479 > 0.2\n", + "0.883 > 0.2\n", + "0.607 > 0.2\n", + "0.767 > 0.2\n", + "0.768 > 0.2\n", + "0.012 <= 0.2\n", + "Finally 0.012\n" ] } ], From 5a13ee2d88fc1958bf3b54357bd40f98174b4017 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Tue, 17 Oct 2023 23:36:59 +0000 Subject: [PATCH 53/97] Format black --- pyiron_workflow/channels.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyiron_workflow/channels.py b/pyiron_workflow/channels.py index 749c78554..9f8a82a91 100644 --- a/pyiron_workflow/channels.py +++ b/pyiron_workflow/channels.py @@ -95,9 +95,8 @@ def _valid_connection(self, other: Channel) -> bool: Connections should have the same generic type, but not the same type -- i.e. they should be an input/output pair of some connection type. """ - return ( - isinstance(other, self.generic_type) - and not isinstance(other, self.__class__) + return isinstance(other, self.generic_type) and not isinstance( + other, self.__class__ ) def connect(self, *others: Channel) -> None: From a07eb78fe86477b4b7505f3742b423faed30fa25 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 11:09:16 -0700 Subject: [PATCH 54/97] Introduce coupling between input channels This will be helpful for making macros into walled gardens that have their own IO instead of simply using references to their children's IO; it will allow us to keep the child node input up-to-date. --- pyiron_workflow/channels.py | 45 +++++++++++++++++++++++++++++++++++++ tests/unit/test_channels.py | 38 +++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/pyiron_workflow/channels.py b/pyiron_workflow/channels.py index 9f8a82a91..1608f9dd3 100644 --- a/pyiron_workflow/channels.py +++ b/pyiron_workflow/channels.py @@ -376,7 +376,10 @@ def __init__( default: typing.Optional[typing.Any] = NotData, type_hint: typing.Optional[typing.Any] = None, strict_connections: bool = True, + value_receiver: typing.Optional[InputData] = None ): + self._value = NotData + self._value_receiver = None super().__init__( label=label, node=node, @@ -384,6 +387,17 @@ def __init__( type_hint=type_hint, ) self.strict_connections = strict_connections + self.value_receiver = value_receiver + + @property + def value(self): + return self._value + + @value.setter + def value(self, new_value): + if self.value_receiver is not None: + self.value_receiver.value = new_value + self._value = new_value def fetch(self) -> None: """ @@ -405,6 +419,37 @@ def fetch(self) -> None: self.value = out.value break + @property + def value_receiver(self) -> InputData: + """ + Another input data channel to whom new values are always pushed (without type + checking of any sort, not even when forming the couple!) + + Useful for macros, so that the input of owned nodes can be kept up-to-date with + the new data values assigned to the macro input. + """ + return self._value_receiver + + @value_receiver.setter + def value_receiver(self, new_partner: InputData): + if new_partner is not None: + + if not isinstance(new_partner, InputData): + raise TypeError( + f"The {self.__class__.__name__} {self.label} got a coupling " + f"partner {new_partner} but requires something of type " + f"{InputData.__name__}" + ) + + if new_partner is self: + raise ValueError( + f"{self.__class__.__name__} {self.label} cannot couple to itself" + ) + + new_partner.value = self.value + + self._value_receiver = new_partner + def activate_strict_connections(self) -> None: self.strict_connections = True diff --git a/tests/unit/test_channels.py b/tests/unit/test_channels.py index f747350c5..c6e711fe5 100644 --- a/tests/unit/test_channels.py +++ b/tests/unit/test_channels.py @@ -234,6 +234,44 @@ def test_ready(self): self.ni1.value = "Not numeric at all" self.assertFalse(self.ni1.ready) + def test_input_coupling(self): + self.assertNotEqual( + self.ni2.value, + 2, + msg="Ensure we start from a setup that the next test is meaningful" + ) + self.ni1.value = 2 + self.ni1.value_receiver = self.ni2 + self.assertEqual( + self.ni2.value, + 2, + msg="Coupled value should get updated on coupling" + ) + self.ni1.value = 3 + self.assertEqual( + self.ni2.value, + 3, + msg="Coupled value should get updated after partner update" + ) + self.ni2.value = 4 + self.assertEqual( + self.ni1.value, + 3, + msg="Coupling is uni-directional, the partner should not push values back" + ) + + with self.assertRaises( + TypeError, + msg="Only input data channels are valid partners" + ): + self.ni1.value_receiver = self.no + + with self.assertRaises( + ValueError, + msg="Must not couple to self to avoid infinite recursion" + ): + self.ni1.value_receiver = self.ni1 + class TestSignalChannels(TestCase): def setUp(self) -> None: From cf81bbdeee843de31a9d5af01564a7c78ba6ed02 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Wed, 18 Oct 2023 18:23:17 +0000 Subject: [PATCH 55/97] Format black --- pyiron_workflow/channels.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyiron_workflow/channels.py b/pyiron_workflow/channels.py index 1608f9dd3..925e64785 100644 --- a/pyiron_workflow/channels.py +++ b/pyiron_workflow/channels.py @@ -376,7 +376,7 @@ def __init__( default: typing.Optional[typing.Any] = NotData, type_hint: typing.Optional[typing.Any] = None, strict_connections: bool = True, - value_receiver: typing.Optional[InputData] = None + value_receiver: typing.Optional[InputData] = None, ): self._value = NotData self._value_receiver = None @@ -433,7 +433,6 @@ def value_receiver(self) -> InputData: @value_receiver.setter def value_receiver(self, new_partner: InputData): if new_partner is not None: - if not isinstance(new_partner, InputData): raise TypeError( f"The {self.__class__.__name__} {self.label} got a coupling " From e120a57f336f52a0280ef5f495d63ed7d74ffd27 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 11:40:55 -0700 Subject: [PATCH 56/97] Improve docstring and type hinting --- pyiron_workflow/composite.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index cd1132593..c740c6d65 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -262,8 +262,26 @@ def _build_io( self, io: Inputs | Outputs, target: Literal["inputs", "outputs"], - key_map: dict[str, str] | None, + key_map: dict[str, str | None] | None, ) -> Inputs | Outputs: + """ + Build an IO panel for exposing child node IO to the outside world at the level + of the composite node's IO. + + Args: + io [Inputs|Outputs]: The IO panel object to populate + target [Literal["inputs", "outputs"]]: Whether this is I or O. + key_map [dict[str, str]|None]: A map between the default convention for + mapping child IO to composite IO (`"{node.label}__{channel.label}"`) and + whatever label you actually want to expose to the composite user. Also + allows non-standards channel exposure, i.e. exposing + internally-connected channels (which would not normally be exposed) by + providing a string-to-string map, or suppressing unconnected channels + (which normally would be exposed) by providing a string-None map. + + Returns: + (Inputs|Outputs): The populated panel. + """ key_map = {} if key_map is None else key_map for node in self.nodes.values(): panel = getattr(node, target) From 2493495f30b03245e3aa671c431c90a4545586c6 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 11:41:54 -0700 Subject: [PATCH 57/97] Refactor: change signature Eliminate redundant information --- pyiron_workflow/composite.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index c740c6d65..3b57731e5 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -260,7 +260,6 @@ def get_data_digraph(self) -> dict[str, set[str]]: def _build_io( self, - io: Inputs | Outputs, target: Literal["inputs", "outputs"], key_map: dict[str, str | None] | None, ) -> Inputs | Outputs: @@ -269,7 +268,6 @@ def _build_io( of the composite node's IO. Args: - io [Inputs|Outputs]: The IO panel object to populate target [Literal["inputs", "outputs"]]: Whether this is I or O. key_map [dict[str, str]|None]: A map between the default convention for mapping child IO to composite IO (`"{node.label}__{channel.label}"`) and @@ -283,6 +281,7 @@ def _build_io( (Inputs|Outputs): The populated panel. """ key_map = {} if key_map is None else key_map + io = Inputs() if target == "inputs" else Outputs() for node in self.nodes.values(): panel = getattr(node, target) for channel_label in panel.labels: @@ -297,10 +296,10 @@ def _build_io( return io def _build_inputs(self) -> Inputs: - return self._build_io(Inputs(), "inputs", self.inputs_map) + return self._build_io("inputs", self.inputs_map) def _build_outputs(self) -> Outputs: - return self._build_io(Outputs(), "outputs", self.outputs_map) + return self._build_io("outputs", self.outputs_map) def add(self, node: Node, label: Optional[str] = None) -> None: """ From 9a599750aa362d2785770efca681b2001734b0e5 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 11:46:14 -0700 Subject: [PATCH 58/97] Refactor: rename variable --- pyiron_workflow/composite.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 3b57731e5..a6c31407d 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -260,7 +260,7 @@ def get_data_digraph(self) -> dict[str, set[str]]: def _build_io( self, - target: Literal["inputs", "outputs"], + i_or_o: Literal["inputs", "outputs"], key_map: dict[str, str | None] | None, ) -> Inputs | Outputs: """ @@ -281,9 +281,9 @@ def _build_io( (Inputs|Outputs): The populated panel. """ key_map = {} if key_map is None else key_map - io = Inputs() if target == "inputs" else Outputs() + io = Inputs() if i_or_o == "inputs" else Outputs() for node in self.nodes.values(): - panel = getattr(node, target) + panel = getattr(node, i_or_o) for channel_label in panel.labels: channel = panel[channel_label] default_key = f"{node.label}__{channel_label}" From 20f5ce221423d1fbe4de362b497c40ce8efdc2b7 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 12:06:28 -0700 Subject: [PATCH 59/97] Break out a line of the IO creation into its own method So that Workflow and Composite can do things differently --- pyiron_workflow/composite.py | 33 ++++++++++++++++++++++++++++++--- pyiron_workflow/macro.py | 8 ++++++++ pyiron_workflow/workflow.py | 11 +++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index a6c31407d..e24680e65 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -5,7 +5,7 @@ from __future__ import annotations -from abc import ABC +from abc import ABC, abstractmethod from functools import partial from typing import Literal, Optional, TYPE_CHECKING @@ -19,7 +19,7 @@ from pyiron_workflow.util import logger, DotDict, SeabornColors if TYPE_CHECKING: - from pyiron_workflow.channels import Channel + from pyiron_workflow.channels import Channel, InputData, OutputData class Composite(Node, ABC): @@ -289,12 +289,39 @@ def _build_io( default_key = f"{node.label}__{channel_label}" try: if key_map[default_key] is not None: - io[key_map[default_key]] = channel + io[key_map[default_key]] = self._get_linking_channel( + channel, key_map[default_key] + ) except KeyError: if not channel.connected: io[default_key] = channel return io + @abstractmethod + def _get_linking_channel( + self, + child_reference_channel: InputData | OutputData, + composite_io_key: str, + ) -> InputData | OutputData: + """ + Returns the channel that will be the link between the provided child channel, + and the composite's IO at the given key. + + The returned channel should be fully compatible with the provided child channel, + i.e. same type, same type hint... (For instance, the child channel itself is a + valid return, which would create a composite IO panel that works by reference.) + + Args: + child_reference_channel (InputData | OutputData): The child channel + composite_io_key (str): The key under which this channel will be stored on + the composite's IO. + + Returns: + (Channel): A channel with the same type, type hint, etc. as the reference + channel passed in. + """ + pass + def _build_inputs(self) -> Inputs: return self._build_io("inputs", self.inputs_map) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index cdea3244a..83e0249bb 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -14,6 +14,7 @@ if TYPE_CHECKING: from bidict import bidict + from pyiron_workflow.channels import InputData, OutputData from pyiron_workflow.node import Node @@ -184,6 +185,13 @@ def __init__( self.update_input(**kwargs) + def _get_linking_channel( + self, + child_reference_channel: InputData | OutputData, + composite_io_key: str, + ) -> InputData | OutputData: + return child_reference_channel + @property def inputs(self) -> Inputs: return self._inputs diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 4ee1be006..878cfc912 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -15,6 +15,7 @@ if TYPE_CHECKING: from bidict import bidict + from pyiron_workflow.channels import InputData, OutputData from pyiron_workflow.node import Node @@ -184,6 +185,16 @@ def __init__( for node in nodes: self.add(node) + def _get_linking_channel( + self, + child_reference_channel: InputData | OutputData, + composite_io_key: str, + ) -> InputData | OutputData: + """ + Build IO by providing direct references to child channels + """ + return child_reference_channel + @property def inputs(self) -> Inputs: return self._build_inputs() From 36b70bbe20af61a88ada1d2cec9ccfd3c1e366c6 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 12:22:05 -0700 Subject: [PATCH 60/97] :bug: apply the channel getter to _both_ mapped and unmapped cases --- pyiron_workflow/composite.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index e24680e65..117d4c91d 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -288,13 +288,12 @@ def _build_io( channel = panel[channel_label] default_key = f"{node.label}__{channel_label}" try: - if key_map[default_key] is not None: - io[key_map[default_key]] = self._get_linking_channel( - channel, key_map[default_key] - ) + io_panel_key = key_map[default_key] except KeyError: - if not channel.connected: - io[default_key] = channel + io_panel_key = default_key + io[key_map[default_key]] = self._get_linking_channel( + channel, io_panel_key + ) return io @abstractmethod From c3ef618facf164f6242fbe38ae4916509634f406 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 12:31:19 -0700 Subject: [PATCH 61/97] :bug: actually use the key you try-excepted to get --- pyiron_workflow/composite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 117d4c91d..1bbc45332 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -291,7 +291,7 @@ def _build_io( io_panel_key = key_map[default_key] except KeyError: io_panel_key = default_key - io[key_map[default_key]] = self._get_linking_channel( + io[io_panel_key] = self._get_linking_channel( channel, io_panel_key ) return io From 4d6ae7ecf998850d98450a910ded0f6d80ee6ada Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 12:51:10 -0700 Subject: [PATCH 62/97] Revert change to io panel creation I wrongly eliminated logic passing over connected channels --- pyiron_workflow/composite.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 1bbc45332..d0fb7a507 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -289,11 +289,14 @@ def _build_io( default_key = f"{node.label}__{channel_label}" try: io_panel_key = key_map[default_key] + io[io_panel_key] = self._get_linking_channel( + channel, io_panel_key + ) except KeyError: - io_panel_key = default_key - io[io_panel_key] = self._get_linking_channel( - channel, io_panel_key - ) + if not channel.connected: + io[default_key] = self._get_linking_channel( + channel, default_key + ) return io @abstractmethod From b1db7f7cc70925888d6a605b15830e40f6ea4fea Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 12:52:31 -0700 Subject: [PATCH 63/97] Revert change to io panel creation I also wrongly eliminated logic passing channels mapped to None --- pyiron_workflow/composite.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index d0fb7a507..2dd126ae7 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -289,9 +289,10 @@ def _build_io( default_key = f"{node.label}__{channel_label}" try: io_panel_key = key_map[default_key] - io[io_panel_key] = self._get_linking_channel( - channel, io_panel_key - ) + if io_panel_key is not None: + io[io_panel_key] = self._get_linking_channel( + channel, io_panel_key + ) except KeyError: if not channel.connected: io[default_key] = self._get_linking_channel( From 2f33746adcec6d0e01a7ee32dc9a422efa8d1fcf Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 12:56:49 -0700 Subject: [PATCH 64/97] Build IO by _value_ in macro --- pyiron_workflow/macro.py | 46 +++++++++++++++++++++++++++++++++++-- pyiron_workflow/workflow.py | 2 +- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index 83e0249bb..0243199ed 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -8,13 +8,14 @@ from functools import partialmethod from typing import Optional, TYPE_CHECKING +from pyiron_workflow.channels import InputData from pyiron_workflow.composite import Composite from pyiron_workflow.io import Outputs, Inputs if TYPE_CHECKING: from bidict import bidict - from pyiron_workflow.channels import InputData, OutputData + from pyiron_workflow.channels import OutputData from pyiron_workflow.node import Node @@ -190,7 +191,26 @@ def _get_linking_channel( child_reference_channel: InputData | OutputData, composite_io_key: str, ) -> InputData | OutputData: - return child_reference_channel + """ + Build IO by value: create a new channel just like the child's channel. + + In the case of input data, we also form a value link from the composite channel + down to the child channel, so that the child will stay up-to-date. + """ + composite_channel = child_reference_channel.__class__( + label=composite_io_key, + node=self, + default=child_reference_channel.default, + type_hint=child_reference_channel.type_hint + ) + composite_channel.value = child_reference_channel.value + + if isinstance(composite_channel, InputData): + composite_channel.strict_connections = \ + child_reference_channel.strict_connections + composite_channel.value_receiver = child_reference_channel + + return composite_channel @property def inputs(self) -> Inputs: @@ -200,6 +220,28 @@ def inputs(self) -> Inputs: def outputs(self) -> Outputs: return self._outputs + def process_run_result(self, run_output): + if run_output is not self.nodes: + # TODO: Set the nodes to the returned nodes, rebuild IO, and rebuild + # composite IO connections (just hard copy, no need to repeat type + # hint checks) + raise NotImplementedError + self._update_output_channels_from_children() + return super().process_run_result(run_output) + + def _update_output_channels_from_children(self): + for composite_key, composite_channel in self.outputs.items(): + try: + default_key = self.outputs_map.inverse[composite_key] + except (AttributeError, KeyError): + # AttributeError: self.outputs_map is None, has no invers + # KeyError: this channel was not specially mapped + default_key = composite_key + node_label = default_key.split("__")[0] + channel_label = default_key.removeprefix(node_label + "__") + composite_channel.value = \ + self.nodes[node_label].outputs[channel_label].value + def _rebuild_data_io(self): self._inputs = self._build_inputs() self._outputs = self._build_outputs() diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 878cfc912..5817fa7e7 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -191,7 +191,7 @@ def _get_linking_channel( composite_io_key: str, ) -> InputData | OutputData: """ - Build IO by providing direct references to child channels + Build IO by reference: just return the child's channel itself. """ return child_reference_channel From ca37c3065a536283f2b94ef561f1a32d3f32683d Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 12:57:27 -0700 Subject: [PATCH 65/97] Update macro tests To reflect the fact that IO is actually reconstructed now on replacement; we can still check for the correct link between macro and child by looking at the value receiver though --- tests/unit/test_macro.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index b8d811339..736f6acb6 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -318,7 +318,7 @@ def add_two(x): msg="Replacement should be reflected in the starting nodes" ) self.assertIs( - macro.inputs.one__x, + macro.inputs.one__x.value_receiver, new_starter.inputs.x, msg="Replacement should be reflected in composite IO" ) From 402ea7d9aa815ad25a4790ce1bc84ff21ad55add Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 13:06:47 -0700 Subject: [PATCH 66/97] Move the value receiver up to DataChannel So it's also available for output --- pyiron_workflow/channels.py | 89 +++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/pyiron_workflow/channels.py b/pyiron_workflow/channels.py index 925e64785..a9887ad76 100644 --- a/pyiron_workflow/channels.py +++ b/pyiron_workflow/channels.py @@ -279,11 +279,54 @@ def __init__( node: Node, default: typing.Optional[typing.Any] = NotData, type_hint: typing.Optional[typing.Any] = None, + value_receiver: typing.Optional[InputData] = None, ): super().__init__(label=label, node=node) + self._value = NotData + self._value_receiver = None self.default = default self.value = default self.type_hint = type_hint + self.value_receiver = value_receiver + + @property + def value(self): + return self._value + + @value.setter + def value(self, new_value): + if self.value_receiver is not None: + self.value_receiver.value = new_value + self._value = new_value + + @property + def value_receiver(self) -> InputData | OutputData | None: + """ + Another data channel of the same type to whom new values are always pushed + (without type checking of any sort, not even when forming the couple!) + + Useful for macros, so that the IO of owned nodes and IO at the macro level can + be kept synchronized. + """ + return self._value_receiver + + @value_receiver.setter + def value_receiver(self, new_partner: InputData | OutputData | None): + if new_partner is not None: + if not isinstance(new_partner, self.__class__): + raise TypeError( + f"The {self.__class__.__name__} {self.label} got a coupling " + f"partner {new_partner} but requires something of the same type" + ) + + if new_partner is self: + raise ValueError( + f"{self.__class__.__name__} {self.label} cannot couple to itself" + ) + + new_partner.value = self.value + + self._value_receiver = new_partner @property def generic_type(self) -> type[Channel]: @@ -375,29 +418,17 @@ def __init__( node: Node, default: typing.Optional[typing.Any] = NotData, type_hint: typing.Optional[typing.Any] = None, - strict_connections: bool = True, value_receiver: typing.Optional[InputData] = None, + strict_connections: bool = True, ): - self._value = NotData - self._value_receiver = None super().__init__( label=label, node=node, default=default, type_hint=type_hint, + value_receiver=value_receiver ) self.strict_connections = strict_connections - self.value_receiver = value_receiver - - @property - def value(self): - return self._value - - @value.setter - def value(self, new_value): - if self.value_receiver is not None: - self.value_receiver.value = new_value - self._value = new_value def fetch(self) -> None: """ @@ -419,36 +450,6 @@ def fetch(self) -> None: self.value = out.value break - @property - def value_receiver(self) -> InputData: - """ - Another input data channel to whom new values are always pushed (without type - checking of any sort, not even when forming the couple!) - - Useful for macros, so that the input of owned nodes can be kept up-to-date with - the new data values assigned to the macro input. - """ - return self._value_receiver - - @value_receiver.setter - def value_receiver(self, new_partner: InputData): - if new_partner is not None: - if not isinstance(new_partner, InputData): - raise TypeError( - f"The {self.__class__.__name__} {self.label} got a coupling " - f"partner {new_partner} but requires something of type " - f"{InputData.__name__}" - ) - - if new_partner is self: - raise ValueError( - f"{self.__class__.__name__} {self.label} cannot couple to itself" - ) - - new_partner.value = self.value - - self._value_receiver = new_partner - def activate_strict_connections(self) -> None: self.strict_connections = True From 604fc56fa07b41802f2477dc6d7f7d39ec0162ec Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 13:11:01 -0700 Subject: [PATCH 67/97] Always keep macro output values synchronized --- pyiron_workflow/macro.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index 0243199ed..7438843cc 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -8,14 +8,13 @@ from functools import partialmethod from typing import Optional, TYPE_CHECKING -from pyiron_workflow.channels import InputData +from pyiron_workflow.channels import InputData, OutputData from pyiron_workflow.composite import Composite from pyiron_workflow.io import Outputs, Inputs if TYPE_CHECKING: from bidict import bidict - from pyiron_workflow.channels import OutputData from pyiron_workflow.node import Node @@ -209,6 +208,12 @@ def _get_linking_channel( composite_channel.strict_connections = \ child_reference_channel.strict_connections composite_channel.value_receiver = child_reference_channel + elif isinstance(composite_channel, OutputData): + child_reference_channel.value_receiver = composite_channel + else: + raise TypeError( + "This should not be an accessible state, please contact the developers" + ) return composite_channel @@ -222,26 +227,11 @@ def outputs(self) -> Outputs: def process_run_result(self, run_output): if run_output is not self.nodes: - # TODO: Set the nodes to the returned nodes, rebuild IO, and rebuild - # composite IO connections (just hard copy, no need to repeat type - # hint checks) + # TODO: Set the nodes to the returned nodes, rebuild IO, and reconnect + # composite IO (just hard copy, no need to repeat type hint checks) raise NotImplementedError - self._update_output_channels_from_children() return super().process_run_result(run_output) - def _update_output_channels_from_children(self): - for composite_key, composite_channel in self.outputs.items(): - try: - default_key = self.outputs_map.inverse[composite_key] - except (AttributeError, KeyError): - # AttributeError: self.outputs_map is None, has no invers - # KeyError: this channel was not specially mapped - default_key = composite_key - node_label = default_key.split("__")[0] - channel_label = default_key.removeprefix(node_label + "__") - composite_channel.value = \ - self.nodes[node_label].outputs[channel_label].value - def _rebuild_data_io(self): self._inputs = self._build_inputs() self._outputs = self._build_outputs() From 08c74cce81e6263b6670b4c25d25edb6eabb3596 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 15:16:35 -0700 Subject: [PATCH 68/97] Rebuild connections when you rebuild IO Since we might be rebuilding because of an internal change, allow for the channels to now differ --- pyiron_workflow/macro.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index 7438843cc..783b89e59 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -233,8 +233,19 @@ def process_run_result(self, run_output): return super().process_run_result(run_output) def _rebuild_data_io(self): + old_inputs = self.inputs + old_outputs = self.outputs self._inputs = self._build_inputs() self._outputs = self._build_outputs() + for old, new in [(old_inputs, self.inputs), (old_outputs, self.outputs)]: + for old_channel in old: + try: + new[old_channel.label].copy_connections(old_channel) + except AttributeError: + # It looks like a key error if `old_channel.label` is not an item, + # but we're actually having __getitem__ fall back on __getattr__ + pass + old_channel.disconnect_all() def _configure_graph_execution(self): run_signals = self.disconnect_run() From 9146b007b53adbebfa15183bf54cdb5323e0c9e4 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 15:17:00 -0700 Subject: [PATCH 69/97] Reexecute demo notebook --- notebooks/workflow_example.ipynb | 127 +++++++++++++++---------------- 1 file changed, 62 insertions(+), 65 deletions(-) diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index 06e536ed2..f6fd2ab9a 100644 --- a/notebooks/workflow_example.ipynb +++ b/notebooks/workflow_example.ipynb @@ -666,8 +666,8 @@ { "data": { "text/plain": [ - "array([0.1153697 , 0.29712504, 0.22636199, 0.1263152 , 0.00630191,\n", - " 0.64039423, 0.73223408, 0.76977259, 0.62491999, 0.52663026])" + "array([0.6816222 , 0.60285251, 0.31984666, 0.38336884, 0.95586544,\n", + " 0.20915899, 0.73614411, 0.67259937, 0.84499503, 0.10539287])" ] }, "execution_count": 23, @@ -676,7 +676,7 @@ }, { "data": { - "image/png": "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rq4uZy9qYshPf/pTM2XKFPPkk0+a5uZmc9ddd5mpU6ea//7v/x62//Hjx80555xj7rrrLtPc3GyefPJJM2XKFPPCCy9McOXhcTrelpYWc+edd5r//M//NJdffrm56667JrbgMXA61rvuuss8+OCD5uDBg+bYsWOmvLzcTJkyxRw+fHiCK3fO6VgPHz5snnvuOXP06FHT0tJinn32WXPOOeeYrVu3TnDlzjkd64BPPvnEXHTRRaa4uNhcdtllE1PsGDkd6+uvv24kmffee8/4/f7B4/Tp0xNcuXPhvK/f+ta3TH5+vqmrqzMtLS3mN7/5jXn77bcnsOrwOB3rJ598EvJ+trW1menTp5uKioqJLTwMTsdaX19vEhISzOOPP26OHz9u6uvrzcKFC83KlSsdvW5MBZVFixaZ0tLSkLZ58+aZjRs3Dtv/X//1X828efNC2m699VZz5ZVXjluNkeR0vF901VVXRVVQGctYByxYsMBUVlZGurSIi8RY/+7v/s6sWrUq0qVFXLhjLSkpMf/2b/9mKioqoiaoOB3rQFD53//93wmoLrKcjvXVV181Ho/HdHV1TUR5ETXWv9eXX37ZuFwu84c//GE8yosop2P94Q9/aC666KKQtieeeMLMnj3b0evGzNJPX1+fGhsbVVxcHNJeXFyshoaGYc/51a9+NaT/tddeq0OHDunzzz8ft1ojIZzxRqtIjLW/v189PT2aPn36eJQYMZEYa1NTkxoaGnTVVVeNR4kRE+5Yn376aX3wwQeqqKgY7xIjZizv6xVXXCGv16slS5bo9ddfH88yIyKcsb7yyivKy8vTD37wA51//vm65JJLdM899+hPf/rTRJQctkj8vW7fvl3XXHONsrKyxqPEiAlnrIWFhfrwww+1b98+GWP08ccf64UXXtB1113n6LWj4ksJR6Ozs1PBYFDp6ekh7enp6Wpvbx/2nPb29mH7nz59Wp2dnfJ6veNW71iFM95oFYmxPvzww/rjH/+oG264YTxKjJixjHX27Nn6n//5H50+fVr333+/1q9fP56ljlk4Y33//fe1ceNG1dfXKykpev75CmesXq9X27Ztk8/nU29vr5599lktWbJEb7zxhr75zW9ORNlhCWesx48f14EDB+R2u/Xyyy+rs7NT3/3ud3Xy5Emrr1MZ679Nfr9fr776qp577rnxKjFiwhlrYWGhdu/erZKSEp06dUqnT5/Wt771Lf3oRz9y9NrR85c+Si6XK+SxMWZI25/rP1y7rZyON5qFO9Y9e/bo/vvv189//nPNmjVrvMqLqHDGWl9fr08//VS//vWvtXHjRn3ta1/Td77znfEsMyJGO9ZgMKibbrpJlZWVuuSSSyaqvIhy8r7OnTtXc+fOHXxcUFCgtrY2PfTQQ1YHlQFOxtrf3y+Xy6Xdu3cPftPuI488ouuvv15btmxRamrquNc7FuH+2/TMM8/o3HPP1cqVK8epsshzMtbm5mbdeeeduu+++3TttdfK7/fr3nvvVWlpqbZv3z7q14yZoDJz5kwlJiYOSXYdHR1DEuCAjIyMYfsnJSVpxowZ41ZrJIQz3mg1lrHW1tZq3bp1ev7553XNNdeMZ5kRMZaxZmdnS5K+/vWv6+OPP9b9999vdVBxOtaenh4dOnRITU1NuuOOOySd+YAzxigpKUmvvfaarr766gmp3alI/b1eeeWV2rVrV6TLi6hwxur1enX++ecPhhRJmj9/vowx+vDDD3XxxRePa83hGsv7aozRjh07tHr1aiUnJ49nmRERzlirqqpUVFSke++9V5J06aWXaurUqVq8eLEeeOCBUa9axMw1KsnJyfL5fKqrqwtpr6urU2Fh4bDnFBQUDOn/2muvKS8vT1OmTBm3WiMhnPFGq3DHumfPHq1du1bPPfec4zXRyRKp99UYo97e3kiXF1FOx5qWlqbf/va3OnLkyOBRWlqquXPn6siRI8rPz5+o0h2L1Pva1NRk9ZK0FN5Yi4qK9NFHH+nTTz8dbDt27JgSEhI0e/bsca13LMbyvr755pv6/e9/r3Xr1o1niRETzlg/++wzJSSExozExERJ/3/1YlQcXXpruYFbp7Zv326am5tNWVmZmTp16uDV1Bs3bjSrV68e7D9we/Ldd99tmpubzfbt26Py9uTRjtcYY5qamkxTU5Px+XzmpptuMk1NTeadd96ZjPIdcTrW5557ziQlJZktW7aE3Ar4ySefTNYQRs3pWH/84x+bV155xRw7dswcO3bM7Nixw6SlpZlNmzZN1hBGLZzf4S+Kprt+nI710UcfNS+//LI5duyYOXr0qNm4caORZF588cXJGsKoOR1rT0+PmT17trn++uvNO++8Y958801z8cUXm/Xr10/WEEYt3N/hVatWmfz8/Ikud0ycjvXpp582SUlJprq62nzwwQfmwIEDJi8vzyxatMjR68ZUUDHGmC1btpisrCyTnJxscnNzzZtvvjn4s5tvvtlcddVVIf3feOMNc8UVV5jk5GRz4YUXmpqamgmueGycjlfSkCMrK2tiiw6Tk7FeddVVw4715ptvnvjCw+BkrE888YRZuHChOeecc0xaWpq54oorTHV1tQkGg5NQuXNOf4e/KJqCijHOxvrggw+aOXPmGLfbbb7yla+Yb3zjG+YXv/jFJFQdHqfv67vvvmuuueYak5qaambPnm02bNhgPvvsswmuOjxOx/rJJ5+Y1NRUs23btgmudOycjvWJJ54wCxYsMKmpqcbr9Zp/+Id/MB9++KGj13QZ42T+BQAAYOLEzDUqAAAg9hBUAACAtQgqAADAWgQVAABgLYIKAACwFkEFAABYi6ACAACsRVABAADWIqgAAABrEVQAAIC1CCoAAMBaBBUAAGCt/wdxUOz/6a0clAAAAABJRU5ErkJggg==", + "image/png": "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", "text/plain": [ "
" ] @@ -932,127 +932,127 @@ "clustersimple\n", "\n", "simple: Workflow\n", - "\n", - "clustersimpleInputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Inputs\n", - "\n", "\n", "clustersimpleOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Outputs\n", "\n", "\n", "clustersimplea\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "a: AddOne\n", "\n", "\n", "clustersimpleaInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Inputs\n", "\n", "\n", "clustersimpleaOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Outputs\n", "\n", "\n", "clustersimpleb\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "b: AddOne\n", "\n", "\n", "clustersimplebInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Inputs\n", "\n", "\n", "clustersimplebOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Outputs\n", "\n", "\n", "clustersimplesum\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "sum: AddNode\n", "\n", "\n", "clustersimplesumInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Inputs\n", "\n", "\n", "clustersimplesumOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Outputs\n", "\n", + "\n", + "clustersimpleInputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", "\n", "\n", "clustersimpleInputsrun\n", @@ -1231,7 +1231,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 29, @@ -1262,7 +1262,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5ee2f89cadea46a8926434ab39f44805", + "model_id": "11fa1336d10a42f4936ce22a299f191d", "version_major": 2, "version_minor": 0 }, @@ -1275,7 +1275,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -1289,7 +1289,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 30, @@ -1541,7 +1541,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 31, @@ -1583,7 +1583,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -1703,6 +1703,7 @@ "\n", "@Workflow.wrap_as.single_value_node()\n", "def per_atom_energy_difference(structure1, energy1, structure2, energy2):\n", + " # The unrelaxed structure is fine, we're just using it to get n_atoms\n", " de = (energy2[-1]/len(structure2)) - (energy1[-1]/len(structure1))\n", " return de" ] @@ -2960,7 +2961,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 37, @@ -3003,7 +3004,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -3044,16 +3045,21 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel job was not connected to job, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel job was not connected to job, andthus could not disconnect from it.\n", + " warn(\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + " warn(\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel element was not connected to user_input, andthus could not disconnect from it.\n", " warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel energy_pot was not connected to energy1, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel structure was not connected to structure1, andthus could not disconnect from it.\n", " warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel energy was not connected to energy1, andthus could not disconnect from it.\n", " warn(\n" ] } ], "source": [ + "replacee = wf.min_phase1.calc \n", "wf.min_phase1.calc = Macro.create.atomistics.CalcStatic" ] }, @@ -3085,7 +3091,7 @@ ], "source": [ "# Bad guess\n", - "out = wf(element=\"Al\", phase1=\"fcc\", phase2=\"hcp\", lattice_guess1=3, lattice_guess2=3)\n", + "out = wf(element=\"Al\", phase1=\"fcc\", phase2=\"hcp\", lattice_guess1=3, lattice_guess2=3.1)\n", "print(f\"{wf.inputs.element.value}: E({wf.inputs.phase2.value}) - E({wf.inputs.phase1.value}) = {out.compare__de:.2f} eV/atom\")" ] }, @@ -3099,7 +3105,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -3115,7 +3121,7 @@ ], "source": [ "# Good guess\n", - "out = wf(element=\"Al\", phase1=\"fcc\", phase2=\"hcp\", lattice_guess1=4.05, lattice_guess2=3)\n", + "out = wf(element=\"Al\", phase1=\"fcc\", phase2=\"hcp\", lattice_guess1=4.05, lattice_guess2=3.2)\n", "print(f\"{wf.inputs.element.value}: E({wf.inputs.phase2.value}) - E({wf.inputs.phase1.value}) = {out.compare__de:.2f} eV/atom\")" ] }, @@ -3240,9 +3246,9 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to true, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel run was not connected to true, andthus could not disconnect from it.\n", " warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n" ] } @@ -3323,21 +3329,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.885 > 0.2\n", - "0.790 > 0.2\n", - "0.395 > 0.2\n", - "0.593 > 0.2\n", - "0.220 > 0.2\n", - "0.440 > 0.2\n", - "0.523 > 0.2\n", - "0.407 > 0.2\n", - "0.479 > 0.2\n", - "0.883 > 0.2\n", - "0.607 > 0.2\n", - "0.767 > 0.2\n", - "0.768 > 0.2\n", - "0.012 <= 0.2\n", - "Finally 0.012\n" + "0.406 > 0.2\n", + "0.999 > 0.2\n", + "0.827 > 0.2\n", + "0.417 > 0.2\n", + "0.120 <= 0.2\n", + "Finally 0.120\n" ] } ], From 80c5e5a86c0225343e0a7f64ff99bd43a3d1c950 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 18 Oct 2023 15:18:05 -0700 Subject: [PATCH 70/97] Hint how to run with executor --- pyiron_workflow/macro.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index 783b89e59..d896f43b2 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -227,8 +227,9 @@ def outputs(self) -> Outputs: def process_run_result(self, run_output): if run_output is not self.nodes: - # TODO: Set the nodes to the returned nodes, rebuild IO, and reconnect - # composite IO (just hard copy, no need to repeat type hint checks) + # self.nodes = run_output + # self._rebuild_data_io() + # TODO: I think it should be that simple, but needs tests raise NotImplementedError return super().process_run_result(run_output) From 92b00a94228cee22dcfd29c1fe0148e246b4c545 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 10:05:33 -0700 Subject: [PATCH 71/97] Swap labels and return the replaced node for easier reversion --- pyiron_workflow/composite.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 2dd126ae7..60159d094 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -424,7 +424,7 @@ def remove(self, node: Node | str) -> list[tuple[Channel, Channel]]: del self.nodes[node.label] return disconnected - def replace(self, owned_node: Node | str, replacement: Node | type[Node]): + def replace(self, owned_node: Node | str, replacement: Node | type[Node]) -> Node: """ Replaces a node currently owned with a new node instance. The replacement must not belong to any other parent or have any connections. @@ -432,6 +432,12 @@ def replace(self, owned_node: Node | str, replacement: Node | type[Node]): channel labels need to match precisely, but additional channels may be present. After replacement, the new node will have the old node's connections, label, and belong to this composite. + The labels are swapped, such that the replaced node gets the name of its + replacement (which might be silly, but is useful in case you want to revert the + change and swap the replaced node back in!) + + If replacement fails for some reason, the replacement and replacing node are + both returned to their original state, and the composite is left unchanged. Args: owned_node (Node|str): The node to replace or its label. @@ -467,13 +473,17 @@ def replace(self, owned_node: Node | str, replacement: Node | type[Node]): f"got {replacement}" ) - replacement.copy_io(owned_node) - replacement.label = owned_node.label + replacement.copy_io(owned_node) # If the replacement is incompatible, we'll + # fail here before we've changed the parent at all. Since the replacement was + # first guaranteed to be an unconnected orphan, there is not yet any permanent + # damage is_starting_node = owned_node in self.starting_nodes self.remove(owned_node) + replacement.label, owned_node.label = owned_node.label, replacement.label self.add(replacement) if is_starting_node: self.starting_nodes.append(replacement) + return owned_node def __setattr__(self, key: str, node: Node): if isinstance(node, Node) and key != "parent": From 275308187fa11da701bd78d1ba20bdd5d03c2254 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 10:06:06 -0700 Subject: [PATCH 72/97] Revert IO reconstruction if it fails --- pyiron_workflow/macro.py | 44 ++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index d896f43b2..d67643104 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -234,19 +234,41 @@ def process_run_result(self, run_output): return super().process_run_result(run_output) def _rebuild_data_io(self): + """ + Try to rebuild the IO. + + If an error is encountered, revert back to the existing IO then raise it. + """ old_inputs = self.inputs old_outputs = self.outputs - self._inputs = self._build_inputs() - self._outputs = self._build_outputs() - for old, new in [(old_inputs, self.inputs), (old_outputs, self.outputs)]: - for old_channel in old: - try: - new[old_channel.label].copy_connections(old_channel) - except AttributeError: - # It looks like a key error if `old_channel.label` is not an item, - # but we're actually having __getitem__ fall back on __getattr__ - pass - old_channel.disconnect_all() + connection_changes = [] # For reversion if there's an error + try: + self._inputs = self._build_inputs() + self._outputs = self._build_outputs() + for old, new in [(old_inputs, self.inputs), (old_outputs, self.outputs)]: + for old_channel in old: + if old_channel.connected: + # If the old channel was connected to stuff, we'd better still + # have a corresponding channel and be able to copy these, or we + # should fail hard. + # But, if it wasn't connected, we don't even care whether or not + # we still have a corresponding channel to copy to + new_channel = new[old_channel.label] + new_channel.copy_connections(old_channel) + swapped_conenctions = old_channel.disconnect_all() # Purge old + connection_changes.append( + (new_channel, old_channel, swapped_conenctions) + ) + except Exception as e: + for new_channel, old_channel, swapped_conenctions in connection_changes: + new_channel.disconnect(*swapped_conenctions) + old_channel.connect(*swapped_conenctions) + self._inputs = old_inputs + self._outputs = old_outputs + e.message = f"Unable to rebuild IO for {self.label}; reverting to old IO." \ + f"{e.message}" + raise e + def _configure_graph_execution(self): run_signals = self.disconnect_run() From 3fe7da04ff05c43b2ee205c290ea1e23dbe6600a Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 10:06:32 -0700 Subject: [PATCH 73/97] Revert replacement in macros if IO construction fails --- pyiron_workflow/macro.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index d67643104..4bcfa00ac 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -297,11 +297,17 @@ def _reconnect_run(self, run_signal_pairs_to_restore): pairs[0].connect(pairs[1]) def replace(self, owned_node: Node | str, replacement: Node | type[Node]): - super().replace(owned_node=owned_node, replacement=replacement) - # Make sure node-level IO is pointing to the new node - self._rebuild_data_io() - # This is brute-force overkill since only the replaced node needs to be updated - # but it's not particularly expensive + replaced_node = super().replace(owned_node=owned_node, replacement=replacement) + try: + # Make sure node-level IO is pointing to the new node and that macro-level + # IO gets safely reconstructed + self._rebuild_data_io() + except Exception as e: + # If IO can't be successfully rebuilt using this node, revert changes and + # raise the exception + self.replace(replacement, replaced_node) # Guaranteed to work since + # replacement in the other direction was already a success + raise e def to_workfow(self): raise NotImplementedError From 84496d5c53668b930387430e96bd224da2c22909 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 10:06:54 -0700 Subject: [PATCH 74/97] Commit the tests used to actually work out the last three commits --- tests/unit/test_macro.py | 112 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 736f6acb6..733818b1d 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -367,6 +367,118 @@ def add_two_incompatible_io(not_x): ): macro.two = add_two_incompatible_io + def test_macro_connections_after_replace(self): + # If the macro-level IO is going to change after replacing a child, + # it had better still be able to recreate all the macro-level IO connections + # For macro IO channels that weren't connected, we don't really care + # If it fails to replace, it had better revert to its original state + + macro = Macro(add_three_macro) + downstream = SingleValue(add_one, x=macro.outputs.three__result) + macro > downstream + macro(one__x=0) + # Or once pull exists: macro.one__x = 0; downstream.pull() + self.assertEqual( + 0 + (1 + 1 + 1) + 1, + downstream.outputs.result.value, + msg="Sanity check that our test setup is what we want: macro->single" + ) + + def add_two(x): + result = x + 2 + return result + compatible_replacement = SingleValue(add_two) + + macro.replace(macro.three, compatible_replacement) + macro(one__x=0) + self.assertEqual( + len(downstream.inputs.x.connections), + 1, + msg="After replacement, the downstream node should still have exactly one " + "connection to the macro" + ) + self.assertIs( + downstream.inputs.x.connections[0], + macro.outputs.three__result, + msg="The one connection should be the living, updated macro IO channel" + ) + self.assertEqual( + 0 + (1 + 1 + 2) + 1, + downstream.outputs.result.value, + msg="The whole flow should still function after replacement, but with the " + "new behaviour (and extra 1 added)" + ) + + def different_signature(x): + # When replacing the final node of add_three_macro, the rebuilt IO will + # no longer have three__result, but rather three__changed_output_label, + # which will break existing macro-level IO if the macro output is connected + changed_output_label = x + 3 + return changed_output_label + + incompatible_replacement = SingleValue( + different_signature, + label="original_label" + ) + with self.assertRaises( + AttributeError, + msg="macro.three__result is connected output, but can't be found in the " + "rebuilt IO, so an exception is expected" + ): + macro.replace(macro.three, incompatible_replacement) + self.assertIs( + macro.three, + compatible_replacement, + msg="Failed replacements should get reverted, putting the original node " + "back" + ) + self.assertIs( + macro.three.outputs.result.value_receiver, + macro.outputs.three__result, + msg="Failed replacements should get reverted, restoring the link between " + "child IO and macro IO" + ) + self.assertIs( + downstream.inputs.x.connections[0], + macro.outputs.three__result, + msg="Failed replacements should get reverted, and macro IO should be as " + "it was before" + ) + self.assertFalse( + incompatible_replacement.connected, + msg="Failed replacements should get reverted, leaving the replacement in " + "its original state" + ) + self.assertEqual( + "original_label", + incompatible_replacement.label, + msg="Failed replacements should get reverted, leaving the replacement in " + "its original state" + ) + macro(one__x=1) # Fresh input to make sure updates are actually going through + self.assertEqual( + 1 + (1 + 1 + 2) + 1, + downstream.outputs.result.value, + msg="Final integration test that replacements get reverted, the macro " + "function and downstream results should be the same as before" + ) + + downstream.disconnect() + macro.replace(macro.three, incompatible_replacement) + self.assertIs( + macro.three, + incompatible_replacement, + msg="Since it is only incompatible with the external connections and we " + "broke those first, replacement is expected to work fine now" + ) + macro(one__x=2) + self.assertEqual( + 2 + (1 + 1 + 3), + macro.outputs.three__changed_output_label.value, + msg="For all to be working, we need the result with the new behaviour " + "at its new location" + ) + if __name__ == '__main__': unittest.main() From 1d57fc356b25acb6f21ed25b1dc44689a49d0fe3 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Thu, 19 Oct 2023 17:08:31 +0000 Subject: [PATCH 75/97] Format black --- pyiron_workflow/channels.py | 2 +- pyiron_workflow/macro.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pyiron_workflow/channels.py b/pyiron_workflow/channels.py index a9887ad76..4cfee805a 100644 --- a/pyiron_workflow/channels.py +++ b/pyiron_workflow/channels.py @@ -426,7 +426,7 @@ def __init__( node=node, default=default, type_hint=type_hint, - value_receiver=value_receiver + value_receiver=value_receiver, ) self.strict_connections = strict_connections diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index 4bcfa00ac..bd980dc4c 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -200,13 +200,14 @@ def _get_linking_channel( label=composite_io_key, node=self, default=child_reference_channel.default, - type_hint=child_reference_channel.type_hint + type_hint=child_reference_channel.type_hint, ) composite_channel.value = child_reference_channel.value if isinstance(composite_channel, InputData): - composite_channel.strict_connections = \ + composite_channel.strict_connections = ( child_reference_channel.strict_connections + ) composite_channel.value_receiver = child_reference_channel elif isinstance(composite_channel, OutputData): child_reference_channel.value_receiver = composite_channel @@ -265,11 +266,12 @@ def _rebuild_data_io(self): old_channel.connect(*swapped_conenctions) self._inputs = old_inputs self._outputs = old_outputs - e.message = f"Unable to rebuild IO for {self.label}; reverting to old IO." \ - f"{e.message}" + e.message = ( + f"Unable to rebuild IO for {self.label}; reverting to old IO." + f"{e.message}" + ) raise e - def _configure_graph_execution(self): run_signals = self.disconnect_run() From 01525b8839d834f9e714e48856e7cd86fdb84f41 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 10:21:47 -0700 Subject: [PATCH 76/97] Fix type hint typo --- pyiron_workflow/composite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 60159d094..46c7f7254 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -106,7 +106,7 @@ def __init__( self._outputs_map = None self.inputs_map = inputs_map self.outputs_map = outputs_map - self.nodes: DotDict[str:Node] = DotDict() + self.nodes: DotDict[str, Node] = DotDict() self.starting_nodes: list[Node] = [] self._creator = self.create self.create = self._owned_creator # Override the create method from the class From ca97ad21f1d33c6cc8fb36f2994a8a7a498a051a Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 10:58:26 -0700 Subject: [PATCH 77/97] Make executor a bool We want to avoid ever having a `concurrent.futures.Executor` as an attribute, because it holds a truly unpickleable thread lock object. Instead, we now pass in information _about_ an executor and instantiate one on the fly if we need it. In the medium term we'll want a more sophisticated interface, i.e. information about how to connect to some existing executor object, or more detailed information on what type of executor to instantiate. But for the sake of getting workflows to work on executors, this trivially bool is enough. --- pyiron_workflow/function.py | 6 ------ pyiron_workflow/meta.py | 2 +- pyiron_workflow/node.py | 11 ++++++++--- tests/unit/test_function.py | 9 ++------- tests/unit/test_workflow.py | 3 ++- 5 files changed, 13 insertions(+), 18 deletions(-) diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index ca43c0323..a60ab8ad9 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -474,12 +474,6 @@ def on_run(self): def run_args(self) -> dict: kwargs = self.inputs.to_value_dict() if "self" in self._input_args: - if self.executor is not None: - raise NotImplementedError( - f"The node {self.label} cannot be run on an executor because it " - f"uses the `self` argument and this functionality is not yet " - f"implemented" - ) kwargs["self"] = self return kwargs diff --git a/pyiron_workflow/meta.py b/pyiron_workflow/meta.py index 948d4bb1d..2f8020e1c 100644 --- a/pyiron_workflow/meta.py +++ b/pyiron_workflow/meta.py @@ -159,7 +159,7 @@ def make_loop(macro): # Connect each body node output to the output interface's respective input for body_node, inp in zip(body_nodes, interface.inputs): inp.connect(body_node.outputs[label]) - if body_node.executor is not None: + if body_node.executor: raise NotImplementedError( "Right now the output interface gets run after each body node," "if the body nodes can run asynchronously we need something " diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 26c50b128..0b4b9c3e0 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -12,6 +12,7 @@ from pyiron_workflow.channels import NotData from pyiron_workflow.draw import Node as GraphvizNode +from pyiron_workflow.executors import CloudpickleProcessPoolExecutor as Executor from pyiron_workflow.files import DirectoryObject from pyiron_workflow.has_to_dict import HasToDict from pyiron_workflow.io import Signals, InputSignal, OutputSignal @@ -182,7 +183,10 @@ def __init__( # TODO: Provide support for actually computing stuff with the executor self.signals = self._build_signal_channels() self._working_directory = None - self.executor = None + self.executor = False + # We call it an executor, but it's just whether to use one. + # This is a simply stop-gap as we work out more sophisticated ways to reference + # (or create) an executor process without ever trying to pickle a `_thread.lock` self.future: None | Future = None @property @@ -291,13 +295,14 @@ def _run(self, finished_callback: callable) -> Any | tuple | Future: Handles the status of the node, and communicating with any remote computing resources. """ - if self.executor is None: + if not self.executor: run_output = self.on_run(**self.run_args) return finished_callback(run_output) else: # Just blindly try to execute -- as we nail down the executor interaction # we'll want to fail more cleanly here. - self.future = self.executor.submit(self.on_run, **self.run_args) + executor = Executor() + self.future = executor.submit(self.on_run, **self.run_args) self.future.add_done_callback(finished_callback) return self.future diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 01cf516d4..65dd9034b 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -4,11 +4,6 @@ import unittest import warnings -# from pyiron_contrib.executors import CloudpickleProcessPoolExecutor as Executor -# from pympipool.mpi.executor import PyMPISingleTaskExecutor as Executor - -from pyiron_workflow.executors import CloudpickleProcessPoolExecutor as Executor - from pyiron_workflow.channels import NotData, ChannelConnectionError from pyiron_workflow.files import DirectoryObject from pyiron_workflow.function import ( @@ -304,7 +299,7 @@ def with_self(self, x: float) -> float: msg="Function functions should be able to modify attributes on the node object." ) - node.executor = Executor() + node.executor = True with self.assertRaises(NotImplementedError): # Submitting node_functions that use self is still raising # TypeError: cannot pickle '_thread.lock' object @@ -398,7 +393,7 @@ def test_return_value(self): ) with self.subTest("Run on executor"): - node.executor = Executor() + node.executor = True return_on_explicit_run = node.run() self.assertIsInstance( diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index 698cf71d8..36e70296e 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -200,7 +200,8 @@ def test_executor(self): # Submitting callables that use self is still raising # TypeError: cannot pickle '_thread.lock' object # For now we just fail cleanly - wf.executor = "literally anything other than None should raise the error" + # TODO: This should actually work now, test it + wf.executor = True def test_parallel_execution(self): wf = Workflow("wf") From d6df8b7c65d4afc2f120f943bb63d1c537ac7a42 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 13:57:04 -0700 Subject: [PATCH 78/97] Refactor composite's run processing So that macro and workflow can lean on the same code, and macro can just extend things a little bit to reflect the fact that it needs to rebuild its IO on unpacking children (workflow _always_ rebuilds its IO, so it's a non-issue) --- pyiron_workflow/composite.py | 15 +++++++++++++-- pyiron_workflow/macro.py | 10 +++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 46c7f7254..7c85b4ab7 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -170,10 +170,21 @@ def run_args(self) -> dict: return {"_nodes": self.nodes, "_starting_nodes": self.starting_nodes} def process_run_result(self, run_output): - # self.nodes = run_output - # Running on an executor will require a more sophisticated idea than above + if run_output is not self.nodes: + # Then we probably ran on a parallel process and have an unpacked future + self._update_children(run_output) return DotDict(self.outputs.to_value_dict()) + def _update_children(self, children_from_another_process: DotDict[str, Node]): + """ + If you receive a new dictionary of children, e.g. from unpacking a futures + object of your own children you sent off to another process for computation, + replace your own nodes with them, and set yourself as their parent. + """ + for child in children_from_another_process.values(): + child.parent = self + self.nodes = children_from_another_process + def disconnect_run(self) -> list[tuple[Channel, Channel]]: """ Disconnect all `signals.input.run` connections on all child nodes. diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index bd980dc4c..1fb567565 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -226,13 +226,9 @@ def inputs(self) -> Inputs: def outputs(self) -> Outputs: return self._outputs - def process_run_result(self, run_output): - if run_output is not self.nodes: - # self.nodes = run_output - # self._rebuild_data_io() - # TODO: I think it should be that simple, but needs tests - raise NotImplementedError - return super().process_run_result(run_output) + def _update_children(self, children_from_another_process): + super()._update_children(children_from_another_process) + self._rebuild_data_io() def _rebuild_data_io(self): """ From 31deb97fa6a9edcc7c22a0d5768fec3e0a5c5dbf Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 13:57:35 -0700 Subject: [PATCH 79/97] Allow composite to update its executor value --- pyiron_workflow/composite.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 7c85b4ab7..19d289a04 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -138,17 +138,6 @@ def _owned_creator(self): """ return OwnedCreator(self, self._creator) - @property - def executor(self) -> None: - return None - - @executor.setter - def executor(self, new_executor): - if new_executor is not None: - raise NotImplementedError( - "Running composite nodes with an executor is not yet supported" - ) - def to_dict(self): return { "label": self.label, From 8a83feb68c8f8943839c50ec5446d9f41c1e3cda Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 13:58:01 -0700 Subject: [PATCH 80/97] Write macro tests to define expected behaviour --- tests/unit/test_macro.py | 72 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 733818b1d..34772985a 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -1,6 +1,8 @@ +from concurrent.futures import Future from functools import partialmethod -import unittest from sys import version_info +from time import sleep +import unittest from pyiron_workflow.channels import NotData from pyiron_workflow.function import SingleValue @@ -479,6 +481,74 @@ def different_signature(x): "at its new location" ) + def test_with_executor(self): + macro = Macro(add_three_macro) + downstream = SingleValue(add_one, x=macro.outputs.three__result) + macro > downstream # Later we can just pull() instead + + original_one = macro.one + macro.executor = True + + self.assertIs( + NotData, + macro.outputs.three__result.value, + msg="Sanity check that test is in right starting condition" + ) + + result = macro(one__x=0) + self.assertIsInstance( + result, + Future, + msg="Should be running as a parallel process" + ) + self.assertIs( + NotData, + downstream.outputs.result.value, + msg="Downstream events should not yet have triggered either, we should wait" + "for the callback when the result is ready" + ) + + returned_nodes = result.result() # Wait for the process to finish + self.assertIsNot( + original_one, + returned_nodes.one, + msg="Executing in a parallel process should be returning new instances" + ) + self.assertIsNot( + returned_nodes.one, + macro.nodes.one, + msg="Returned nodes should be taken as children" + ) + self.assertIs( + macro, + macro.nodes.one.parent, + msg="Returned nodes should get the macro as their parent" + ) + self.assertIsNone( + original_one.parent, + msg="Original nodes should be orphaned" + # Note: At time of writing, this is accomplished in Node.__getstate__, + # which feels a bit dangerous... + ) + self.assertEqual( + 0 + 3, + macro.outputs.three__result.value, + msg="And of course we expect the calculation to actually run" + ) + self.assertIs( + downstream.inputs.x.connections[0], + macro.outputs.three__result, + msg="The macro should still be connected to " + ) + sleep(0.2) # Give a moment for the ran signal to emit and downstream to run + # I'm a bit surprised this sleep is necessary + self.assertEqual( + 0 + 3 + 1, + downstream.outputs.result.value, + msg="The finishing callback should also fire off the ran signal triggering" + "downstream execution" + ) + if __name__ == '__main__': unittest.main() From 579622734290961c8eb3a0c6237a76bf2c099102 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 13:58:21 -0700 Subject: [PATCH 81/97] Mess with __getstate__ and __setstate__ until they work --- pyiron_workflow/composite.py | 7 +++++++ pyiron_workflow/io.py | 5 +++++ pyiron_workflow/node.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 19d289a04..d7d927638 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -499,6 +499,7 @@ def __setattr__(self, key: str, node: Node): super().__setattr__(key, node) def __getattr__(self, key): + print(f"{self.__class__.__name__} is trying to get", key) try: return self.nodes[key] except KeyError: @@ -558,6 +559,12 @@ def __getattr__(self, item): return value + def __setstate__(self, state): + # Because we override getattr, we need to use __dict__ assignment directly in + # __setstate__ + self.__dict__["_parent"] = state["_parent"] + self.__dict__["_creator"] = state["_creator"] + class OwnedNodePackage: """ diff --git a/pyiron_workflow/io.py b/pyiron_workflow/io.py index 20e1ca79c..547c6c25f 100644 --- a/pyiron_workflow/io.py +++ b/pyiron_workflow/io.py @@ -161,6 +161,11 @@ def to_dict(self): "channels": {l: c.to_dict() for l, c in self.channel_dict.items()}, } + def __setstate__(self, state): + # Because we override getattr, we need to use __dict__ assignment directly in + # __setstate__ the same way we need it in __init__ + self.__dict__["channel_dict"] = state["channel_dict"] + class DataIO(IO, ABC): """ diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 0b4b9c3e0..5a64c6096 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -610,3 +610,31 @@ def replace_with(self, other: Node | type[Node]): self.parent.replace(self, other) else: warnings.warn(f"Could not replace {self.label}, as it has no parent.") + + def __getstate__(self): + state = self.__dict__ + state["parent"] = None + # I am not at all confident that removing the parent here is the _right_ + # solution. + # In order to run composites on a parallel process, we ship off just the nodes + # and starting nodes. + # When the parallel process returns these, they're obviously different + # instances, so we re-parent them back to the receiving composite. + # At the same time, we want to make sure that the _old_ children get orphaned. + # Of course, we could do that directly in the composite method, but it also + # works to do it here. + # Something I like about this, is it also means that when we ship groups of + # nodes off to another process with cloudpickle, they're definitely not lugging + # along their parent, its connections, etc. with them! + # This is all working nicely as demonstrated over in the macro test suite. + # However, I have a bit of concern that when we start thinking about + # serialization for storage instead of serialization to another process, this + # might introduce a hard-to-track-down bug. + # For now, it works and I'm going to be super pragmatic and go for it, but + # for the record I am admitting that the current shallowness of my understanding + # may cause me/us headaches in the future. + # -Liam + return self.__dict__ + + def __setstate__(self, state): + self.__dict__ = state \ No newline at end of file From f119489713119490dc1f07efc023846770fc43d8 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 14:10:17 -0700 Subject: [PATCH 82/97] :bug: remove debug print --- pyiron_workflow/composite.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index d7d927638..a7a22f334 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -499,7 +499,6 @@ def __setattr__(self, key: str, node: Node): super().__setattr__(key, node) def __getattr__(self, key): - print(f"{self.__class__.__name__} is trying to get", key) try: return self.nodes[key] except KeyError: From b1a55514f22918339a1fb163d2a86c5fe9fc6c14 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 14:18:24 -0700 Subject: [PATCH 83/97] :bug: switch IsNot to Is then comment out the test I had a typo in what I wanted to test for, but it turns out that the test is ill-conceived anyhow, because the futures object is returning new instances each time I ask for the `.result()`, so the `.result()` that the macro got internally will differ from the one in the test --- tests/unit/test_macro.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 34772985a..d4ab485f3 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -514,11 +514,11 @@ def test_with_executor(self): returned_nodes.one, msg="Executing in a parallel process should be returning new instances" ) - self.assertIsNot( - returned_nodes.one, - macro.nodes.one, - msg="Returned nodes should be taken as children" - ) + # self.assertIs( + # returned_nodes.one, + # macro.nodes.one, + # msg="Returned nodes should be taken as children" + # ) # You can't do this, result.result() is returning new instances each call self.assertIs( macro, macro.nodes.one.parent, From 7f2d74bd2575f4d6cdc78baa1ca229b1b6ddc561 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 14:20:17 -0700 Subject: [PATCH 84/97] Test that the workflow will work with an executor as well These are a little simpler tests, as the workflow can't have siblings it's connected to --- tests/unit/test_workflow.py | 53 +++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index 36e70296e..0c0e09ae8 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -1,6 +1,7 @@ -import unittest +from concurrent.futures import Future from sys import version_info from time import sleep +import unittest from bidict import ValueDuplicationError @@ -194,14 +195,50 @@ def test_no_parents(self): # Setting a non-None value to parent raises the type error from the setter wf2.parent = wf - def test_executor(self): + def test_with_executor(self): + wf = Workflow("wf") - with self.assertRaises(NotImplementedError): - # Submitting callables that use self is still raising - # TypeError: cannot pickle '_thread.lock' object - # For now we just fail cleanly - # TODO: This should actually work now, test it - wf.executor = True + wf.a = wf.create.SingleValue(plus_one) + wf.b = wf.create.SingleValue(plus_one, x=wf.a) + + original_a = wf.a + wf.executor = True + + self.assertIs( + NotData, + wf.outputs.b__y.value, + msg="Sanity check that test is in right starting condition" + ) + + result = wf(a__x=0) + self.assertIsInstance( + result, + Future, + msg="Should be running as a parallel process" + ) + + returned_nodes = result.result() # Wait for the process to finish + self.assertIsNot( + original_a, + returned_nodes.a, + msg="Executing in a parallel process should be returning new instances" + ) + self.assertIs( + wf, + wf.nodes.a.parent, + msg="Returned nodes should get the macro as their parent" + ) + self.assertIsNone( + original_a.parent, + msg="Original nodes should be orphaned" + # Note: At time of writing, this is accomplished in Node.__getstate__, + # which feels a bit dangerous... + ) + self.assertEqual( + 0 + 1 + 1, + wf.outputs.b__y.value, + msg="And of course we expect the calculation to actually run" + ) def test_parallel_execution(self): wf = Workflow("wf") From ba4030af700056320bfce32c5935e9f891a9a970 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 14:22:44 -0700 Subject: [PATCH 85/97] Tidy imports --- pyiron_workflow/interfaces.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index 36cd2c858..d38300546 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -8,9 +8,7 @@ from pyiron_base.interfaces.singleton import Singleton -# from pyiron_contrib.executors import CloudpickleProcessPoolExecutor as Executor # from pympipool.mpi.executor import PyMPISingleTaskExecutor as Executor - from pyiron_workflow.executors import CloudpickleProcessPoolExecutor as Executor from pyiron_workflow.function import ( From 5debae1b8b4a1d9e026201579559e1b4505d4b8c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 14:24:21 -0700 Subject: [PATCH 86/97] Update node docstring --- pyiron_workflow/node.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 5a64c6096..8dd69ee09 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -104,12 +104,13 @@ class Node(HasToDict, ABC): Their value is controlled automatically in the defined `run` and `finish_run` methods. - Nodes can be run on the main python process that owns them, or by assigning an - appropriate executor to their `executor` attribute. + Nodes can be run on the main python process that owns them, or by setting their + `executor` attribute to `True`, in which case a + `pyiron_workflow.executors.CloudPickleExecutor` will be used to run the node on a + new process on a single core (in the future, the interface will look a little + different and you'll have more options than that). In case they are run with an executor, their `future` attribute will be populated with the resulting future object. - WARNING: Executors are currently only working when the node executable function does - not use `self`. This is an abstract class. Children *must* define how `inputs` and `outputs` are constructed, and what will From 99806ccd26e74089142b2135e77418391cbfe7d5 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 14:38:51 -0700 Subject: [PATCH 87/97] Fail hard if a function node tries to use self with an executor I don't know how to update self attributes right now --- pyiron_workflow/function.py | 5 +++++ tests/unit/test_function.py | 10 ++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index a60ab8ad9..34b85c91c 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -474,6 +474,11 @@ def on_run(self): def run_args(self) -> dict: kwargs = self.inputs.to_value_dict() if "self" in self._input_args: + if self.executor: + raise ValueError( + f"Function node {self.label} uses the `self` argument, but this " + f"can't yet be run with executors" + ) kwargs["self"] = self return kwargs diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 65dd9034b..4f2958996 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -300,11 +300,13 @@ def with_self(self, x: float) -> float: ) node.executor = True - with self.assertRaises(NotImplementedError): - # Submitting node_functions that use self is still raising - # TypeError: cannot pickle '_thread.lock' object - # For now we just fail cleanly + with self.assertRaises( + ValueError, + msg="We haven't implemented any way to update a function node's `self` when" + "it runs on an executor, so trying to do so should fail hard" + ): node.run() + node.executor = False def with_messed_self(x: float, self) -> float: return x + 0.1 From bbab1cf89848c5effe9871781d76d52f2591149c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 14:39:27 -0700 Subject: [PATCH 88/97] Add back the warning about executors and self --- pyiron_workflow/node.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 8dd69ee09..73ff22d89 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -111,6 +111,8 @@ class Node(HasToDict, ABC): different and you'll have more options than that). In case they are run with an executor, their `future` attribute will be populated with the resulting future object. + WARNING: Executors are currently only working when the node executable function does + not use `self`. This is an abstract class. Children *must* define how `inputs` and `outputs` are constructed, and what will From c6ee4ac2416353b14b59f5e985fe77f7901ce017 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Thu, 19 Oct 2023 21:59:04 +0000 Subject: [PATCH 89/97] Format black --- pyiron_workflow/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 73ff22d89..1199d3802 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -640,4 +640,4 @@ def __getstate__(self): return self.__dict__ def __setstate__(self, state): - self.__dict__ = state \ No newline at end of file + self.__dict__ = state From 7cc3e19b68cf1abe5afa1048645fffe1654e4e1c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 19 Oct 2023 16:09:35 -0700 Subject: [PATCH 90/97] Implement __getstate__ for compatibility with python <3.11 --- pyiron_workflow/composite.py | 4 ++++ pyiron_workflow/io.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index a7a22f334..fef96c5da 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -558,6 +558,10 @@ def __getattr__(self, item): return value + def __getstate__(self): + # Compatibility with python <3.11 + return self.__dict__ + def __setstate__(self, state): # Because we override getattr, we need to use __dict__ assignment directly in # __setstate__ diff --git a/pyiron_workflow/io.py b/pyiron_workflow/io.py index 547c6c25f..f9b341dbf 100644 --- a/pyiron_workflow/io.py +++ b/pyiron_workflow/io.py @@ -161,6 +161,10 @@ def to_dict(self): "channels": {l: c.to_dict() for l, c in self.channel_dict.items()}, } + def __getstate__(self): + # Compatibility with python <3.11 + return self.__dict__ + def __setstate__(self, state): # Because we override getattr, we need to use __dict__ assignment directly in # __setstate__ the same way we need it in __init__ From ba9a9a2f3382bf760f9291ce0adb119641edc44b Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 20 Oct 2023 11:58:27 -0700 Subject: [PATCH 91/97] Disable node package registration and simplify atomistics and standard So that the workflows and macros play well with executors --- pyiron_workflow/interfaces.py | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index d38300546..105344142 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -58,23 +58,15 @@ def Workflow(self): @property def standard(self): - try: - return self._standard - except AttributeError: - from pyiron_workflow.node_library.standard import nodes - - self.register("_standard", *nodes) - return self._standard + from pyiron_workflow.node_package import NodePackage + from pyiron_workflow.node_library.standard import nodes + return NodePackage(*nodes) @property def atomistics(self): - try: - return self._atomistics - except AttributeError: - from pyiron_workflow.node_library.atomistics import nodes - - self.register("_atomistics", *nodes) - return self._atomistics + from pyiron_workflow.node_package import NodePackage + from pyiron_workflow.node_library.atomistics import nodes + return NodePackage(*nodes) @property def meta(self): @@ -85,11 +77,15 @@ def meta(self): return self._meta def register(self, domain: str, *nodes: list[type[Node]]): - if domain in self.__dir__(): - raise AttributeError(f"{domain} is already an attribute of {self}") - from pyiron_workflow.node_package import NodePackage - - setattr(self, domain, NodePackage(*nodes)) + raise NotImplementedError( + "Registering new node packages is currently not playing well with " + "executors. We hope to return this feature soon." + ) + # if domain in self.__dir__(): + # raise AttributeError(f"{domain} is already an attribute of {self}") + # from pyiron_workflow.node_package import NodePackage + # + # setattr(self, domain, NodePackage(*nodes)) class Wrappers(metaclass=Singleton): From 13f3d298f4f567b3f7b33ab4f46ce79afce956bb Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 20 Oct 2023 11:58:38 -0700 Subject: [PATCH 92/97] Things seem fine but add a note --- tests/unit/test_macro.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index d4ab485f3..73bae0f59 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -523,6 +523,9 @@ def test_with_executor(self): macro, macro.nodes.one.parent, msg="Returned nodes should get the macro as their parent" + # Once upon a time there was some evidence that this test was failing + # stochastically, but I just ran the whole test suite 6 times and this test + # 8 times and it always passed fine, so maybe the issue is resolved... ) self.assertIsNone( original_one.parent, From b9a9e7e4bb6c73e0cd56b7d74f98a480f9dc9c68 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Fri, 20 Oct 2023 19:05:09 +0000 Subject: [PATCH 93/97] Format black --- pyiron_workflow/interfaces.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index 105344142..02d826de8 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -60,12 +60,14 @@ def Workflow(self): def standard(self): from pyiron_workflow.node_package import NodePackage from pyiron_workflow.node_library.standard import nodes + return NodePackage(*nodes) @property def atomistics(self): from pyiron_workflow.node_package import NodePackage from pyiron_workflow.node_library.atomistics import nodes + return NodePackage(*nodes) @property From 857eb940b41689616a01a45eeb01bcd233ecf477 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Sat, 21 Oct 2023 20:42:52 -0700 Subject: [PATCH 94/97] Manually set __get/setstate__ in DotDict for python <3.11 --- pyiron_workflow/util.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyiron_workflow/util.py b/pyiron_workflow/util.py index 61dae6c7c..d8e435c90 100644 --- a/pyiron_workflow/util.py +++ b/pyiron_workflow/util.py @@ -13,6 +13,12 @@ def __setattr__(self, key, value): def __dir__(self): return set(super().__dir__() + list(self.keys())) + def __getstate__(self): + return self.__dict__ + + def __setstate(self, state): + self.__dict__ = state + class SeabornColors: """ From 69d15c3a95ec665e1da42f928ec5f431141a7349 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Sat, 21 Oct 2023 20:45:33 -0700 Subject: [PATCH 95/97] :bug: fig typo in method name --- pyiron_workflow/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_workflow/util.py b/pyiron_workflow/util.py index d8e435c90..4002bdc89 100644 --- a/pyiron_workflow/util.py +++ b/pyiron_workflow/util.py @@ -16,7 +16,7 @@ def __dir__(self): def __getstate__(self): return self.__dict__ - def __setstate(self, state): + def __setstate__(self, state): self.__dict__ = state From a64dc95eee12f7337f96a0f18021f729f25213ea Mon Sep 17 00:00:00 2001 From: liamhuber Date: Sat, 21 Oct 2023 20:46:44 -0700 Subject: [PATCH 96/97] Exhaustively set __get/setstate__ everywhere we override __getattr__ These can for sure be dropped when support for <=3.10 is dropped, maybe before. --- pyiron_workflow/composite.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index fef96c5da..1bb1abc29 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -584,3 +584,9 @@ def __getattr__(self, item): if issubclass(value, Node): value = partial(value, parent=self._parent) return value + + def __getstate__(self): + return self.__dict__ + + def __setstate__(self, state): + self.__dict__ = state From 5d0f39b9c6eba9c8ce479aee222d4fdcd3327c0e Mon Sep 17 00:00:00 2001 From: liamhuber Date: Sat, 21 Oct 2023 20:56:48 -0700 Subject: [PATCH 97/97] :bug: have __setstate__ play nicely with __setattr__ Works for 3.11 at least --- pyiron_workflow/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/util.py b/pyiron_workflow/util.py index 4002bdc89..eff32b27d 100644 --- a/pyiron_workflow/util.py +++ b/pyiron_workflow/util.py @@ -17,7 +17,8 @@ def __getstate__(self): return self.__dict__ def __setstate__(self, state): - self.__dict__ = state + for k, v in state.items(): + self.__dict__[k] = v class SeabornColors: