diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index f6fd2ab9..229760f8 100644 --- a/notebooks/workflow_example.ipynb +++ b/notebooks/workflow_example.ipynb @@ -119,8 +119,7 @@ "metadata": {}, "outputs": [], "source": [ - "# pm_node.run()\n", - "pm_node.update()" + "# pm_node.run()" ] }, { @@ -128,9 +127,7 @@ "id": "48b0db5a-548e-4195-8361-76763ddf0474", "metadata": {}, "source": [ - "Using the softer `update()` call checks to make sure the input is `ready` before moving on to `run()`, avoiding this error. In this case, `update()` sees we have no input an aborts by returning `None`.\n", - "\n", - "(Note: If you _do_ swap `update()` to `run()` in this cell, not only will you get the expected error, but `pm_node` will also set its `failed` attribute to `True` -- this will prevent it from being `ready` again until you manually reset `pm_node.failed = False`.)" + "Not only will you get the expected error, but `pm_node` will also set its `failed` attribute to `True` -- this will prevent it from being `ready` again until you manually reset `pm_node.failed = False`." ] }, { @@ -176,7 +173,7 @@ "id": "c54a691e-a075-4d41-bc0f-3a990857a27a", "metadata": {}, "source": [ - "Alternatively, the `run()` command (and `update()` when it proceeds to execution) just return the function's return value:" + "Alternatively, the `run()` command just return the function's return value:" ] }, { @@ -241,7 +238,7 @@ "id": "58ed9b25-6dde-488d-9582-d49d405793c6", "metadata": {}, "source": [ - "This node also exploits type hinting! `run()` will always force the execution, but `update()` will not only check if the data is there, but also if it is the right type:" + "This node also exploits type hinting! `run()` will check that input values conform to type hints before computing anything. Failing at this stage won't actually cause the node to have a `failed` status, so you can just re-run it once the input is fixed." ] }, { @@ -249,46 +246,27 @@ "execution_count": 10, "id": "ac0fe993-6c82-48c8-a780-cbd0c97fc386", "metadata": {}, - "outputs": [], - "source": [ - "adder_node.inputs.x = \"not an integer\"\n", - "adder_node.inputs.x.type_hint, type(adder_node.inputs.x.value)\n", - "adder_node.update()\n", - "# No error because the update doesn't trigger a run since the type hint is not satisfied" - ] - }, - { - "cell_type": "markdown", - "id": "2737de39-6e75-44e1-b751-6315afe5c676", - "metadata": {}, - "source": [ - "Since the execution never happened, the output is unchanged" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "bcbd17f1-a3e4-44f0-bde1-cbddc51c5d73", - "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "1" + "(int, str)" ] }, - "execution_count": 11, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "adder_node.outputs.sum_.value" + "adder_node.inputs.x = \"not an integer\"\n", + "adder_node.inputs.x.type_hint, type(adder_node.inputs.x.value)\n", + "# adder_node.run()" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "id": "15742a49-4c23-4d4a-84d9-9bf19677544c", "metadata": {}, "outputs": [ @@ -298,14 +276,14 @@ "3" ] }, - "execution_count": 12, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "adder_node.inputs.x = 2\n", - "adder_node.update()" + "adder_node.run()" ] }, { @@ -318,7 +296,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "id": "0c8f09a7-67c4-4c6c-a021-e3fea1a16576", "metadata": {}, "outputs": [ @@ -328,7 +306,7 @@ "30" ] }, - "execution_count": 13, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -348,7 +326,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "id": "69b59737-9e09-4b4b-a0e2-76a09de02c08", "metadata": {}, "outputs": [ @@ -358,7 +336,7 @@ "31" ] }, - "execution_count": 14, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -391,7 +369,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 14, "id": "61b43a9b-8dad-48b7-9194-2045e465793b", "metadata": {}, "outputs": [], @@ -401,7 +379,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 15, "id": "647360a9-c971-4272-995c-aa01e5f5bb83", "metadata": {}, "outputs": [ @@ -438,7 +416,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 16, "id": "b8c845b7-7088-43d7-b106-7a6ba1c571ec", "metadata": {}, "outputs": [ @@ -482,7 +460,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 17, "id": "2e418abf-7059-4e1e-9b9f-b3dc0a4b5e35", "metadata": { "tags": [] @@ -532,7 +510,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 18, "id": "59c29856-c77e-48a1-9f17-15d4c58be588", "metadata": {}, "outputs": [ @@ -568,7 +546,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 19, "id": "1a4e9693-0980-4435-aecc-3331d8b608dd", "metadata": {}, "outputs": [], @@ -580,7 +558,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 20, "id": "7c4d314b-33bb-4a67-bfb9-ed77fba3949c", "metadata": {}, "outputs": [ @@ -619,7 +597,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 21, "id": "61ae572f-197b-4a60-8d3e-e19c1b9cc6e2", "metadata": {}, "outputs": [ @@ -659,24 +637,24 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 22, "id": "6569014a-815b-46dd-8b47-4e1cd4584b3b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "array([0.6816222 , 0.60285251, 0.31984666, 0.38336884, 0.95586544,\n", - " 0.20915899, 0.73614411, 0.67259937, 0.84499503, 0.10539287])" + "array([0.91077351, 0.33860412, 0.59806048, 0.66528464, 0.80125293,\n", + " 0.31981677, 0.54395521, 0.4926537 , 0.52626431, 0.7848854 ])" ] }, - "execution_count": 23, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -732,7 +710,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 23, "id": "1cd000bd-9b24-4c39-9cac-70a3291d0660", "metadata": {}, "outputs": [], @@ -759,7 +737,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 24, "id": "7964df3c-55af-4c25-afc5-9e07accb606a", "metadata": {}, "outputs": [ @@ -800,7 +778,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 25, "id": "809178a5-2e6b-471d-89ef-0797db47c5ad", "metadata": {}, "outputs": [ @@ -854,7 +832,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 26, "id": "52c48d19-10a2-4c48-ae81-eceea4129a60", "metadata": {}, "outputs": [ @@ -864,7 +842,7 @@ "{'ay': 3, 'a + b + 2': 7}" ] }, - "execution_count": 27, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -884,7 +862,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 27, "id": "bb35ba3e-602d-4c9c-b046-32da9401dd1c", "metadata": {}, "outputs": [ @@ -894,7 +872,7 @@ "(7, 3)" ] }, - "execution_count": 28, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -913,7 +891,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 28, "id": "2b0d2c85-9049-417b-8739-8a8432a1efbe", "metadata": {}, "outputs": [ @@ -932,127 +910,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,10 +1209,10 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 29, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -1255,14 +1233,14 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 29, "id": "ae500d5e-e55b-432c-8b5f-d5892193cdf5", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "11fa1336d10a42f4936ce22a299f191d", + "model_id": "a289b513c50d41989670c5b4ac9df823", "version_major": 2, "version_minor": 0 }, @@ -1289,10 +1267,10 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 30, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" }, @@ -1333,7 +1311,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 30, "id": "2114d0c3-cdad-43c7-9ffa-50c36d56d18f", "metadata": {}, "outputs": [ @@ -1541,10 +1519,10 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 31, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -1565,7 +1543,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 31, "id": "c71a8308-f8a1-4041-bea0-1c841e072a6d", "metadata": {}, "outputs": [], @@ -1575,7 +1553,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 32, "id": "2b9bb21a-73cd-444e-84a9-100e202aa422", "metadata": {}, "outputs": [ @@ -1593,7 +1571,7 @@ "13" ] }, - "execution_count": 33, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } @@ -1632,7 +1610,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 33, "id": "3668f9a9-adca-48a4-84ea-13add965897c", "metadata": {}, "outputs": [ @@ -1642,7 +1620,7 @@ "{'intermediate': 102, 'plus_three': 103}" ] }, - "execution_count": 34, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } @@ -1680,7 +1658,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 34, "id": "9aaeeec0-5f88-4c94-a6cc-45b56d2f0111", "metadata": {}, "outputs": [], @@ -1710,7 +1688,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 35, "id": "a832e552-b3cc-411a-a258-ef21574fc439", "metadata": {}, "outputs": [], @@ -1737,7 +1715,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 36, "id": "b764a447-236f-4cb7-952a-7cba4855087d", "metadata": {}, "outputs": [ @@ -2961,10 +2939,10 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 37, + "execution_count": 36, "metadata": {}, "output_type": "execute_result" } @@ -2975,7 +2953,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 37, "id": "b51bef25-86c5-4d57-80c1-ab733e703caf", "metadata": {}, "outputs": [ @@ -2996,7 +2974,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 38, "id": "091e2386-0081-436c-a736-23d019bd9b91", "metadata": {}, "outputs": [ @@ -3037,7 +3015,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 39, "id": "4cdffdca-48d3-4486-9045-48102c7e5f31", "metadata": {}, "outputs": [ @@ -3075,7 +3053,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 40, "id": "ed4a3a22-fc3a-44c9-9d4f-c65bc1288889", "metadata": {}, "outputs": [ @@ -3097,7 +3075,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 41, "id": "5a985cbf-c308-4369-9223-b8a37edb8ab1", "metadata": {}, "outputs": [ @@ -3187,7 +3165,7 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 42, "id": "0b373764-b389-4c24-8086-f3d33a4f7fd7", "metadata": {}, "outputs": [ @@ -3201,7 +3179,7 @@ " 17.230249999999995]" ] }, - "execution_count": 43, + "execution_count": 42, "metadata": {}, "output_type": "execute_result" } @@ -3238,7 +3216,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 43, "id": "0dd04b4c-e3e7-4072-ad34-58f2c1e4f596", "metadata": {}, "outputs": [ @@ -3297,7 +3275,7 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 44, "id": "2dfb967b-41ac-4463-b606-3e315e617f2a", "metadata": {}, "outputs": [ @@ -3321,7 +3299,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 45, "id": "2e87f858-b327-4f6b-9237-c8a557f29aeb", "metadata": {}, "outputs": [ @@ -3329,12 +3307,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "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" + "0.064 <= 0.2\n", + "Finally 0.064\n" ] } ], diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 1bb1abc2..270992e0 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -10,12 +10,12 @@ from typing import Literal, Optional, TYPE_CHECKING from bidict import bidict -from toposort import toposort_flatten, CircularDependencyError from pyiron_workflow.interfaces import Creator, Wrappers from pyiron_workflow.io import Outputs, Inputs from pyiron_workflow.node import Node from pyiron_workflow.node_package import NodePackage +from pyiron_workflow.topology import set_run_connections_according_to_linear_dag from pyiron_workflow.util import logger, DotDict, SeabornColors if TYPE_CHECKING: @@ -189,74 +189,11 @@ def disconnect_run(self) -> list[tuple[Channel, Channel]]: def set_run_signals_to_dag_execution(self): """ Disconnects all `signals.input.run` connections among children and attempts to - reconnect these according to the DAG flow of the data. - - Raises: - ValueError: When the data connections do not form a DAG. - """ - self.disconnect_run() - self._set_run_connections_and_starting_nodes_according_to_linear_dag() - # TODO: Replace this linear setup with something more powerful - - def _set_run_connections_and_starting_nodes_according_to_linear_dag(self): - # This is the most primitive sort of topological exploitation we can do - # It is not efficient if the nodes have executors and can run in parallel - try: - # Topological sorting ensures that all input dependencies have been - # executed before the node depending on them gets run - # The flattened part is just that we don't care about topological - # generations that are mutually independent (inefficient but easier for now) - execution_order = toposort_flatten(self.get_data_digraph()) - except CircularDependencyError as e: - raise ValueError( - f"Detected a cycle in the data flow topology, unable to automate the " - f"execution of non-DAGs: cycles found among {e.data}" - ) - - for i, label in enumerate(execution_order[:-1]): - next_node = execution_order[i + 1] - self.nodes[label] > self.nodes[next_node] - self.starting_nodes = [self.nodes[execution_order[0]]] - - def get_data_digraph(self) -> dict[str, set[str]]: - """ - Builds a directed graph of node labels based on data connections between nodes - directly owned by this composite -- i.e. does not worry about data connections - which are entirely internal to an owned sub-graph. - - Returns: - dict[str, set[str]]: A dictionary of nodes and the nodes they depend on for - data. - - Raises: - ValueError: When a node appears in its own input. + reconnect these according to the DAG flow of the data. On success, sets the + starting nodes to just be the upstream-most node in this linear DAG flow. """ - digraph = {} - - for node in self.nodes.values(): - node_dependencies = [] - for channel in node.inputs: - locally_scoped_dependencies = [] - for upstream in channel.connections: - if upstream.node.parent is self: - locally_scoped_dependencies.append(upstream.node.label) - elif channel.node.get_first_shared_parent(upstream.node) is self: - locally_scoped_dependencies.append( - upstream.node.get_parent_proximate_to(self).label - ) - node_dependencies.extend(locally_scoped_dependencies) - node_dependencies = set(node_dependencies) - if node.label in node_dependencies: - # the toposort library has a - # [known issue](https://gitlab.com/ericvsmith/toposort/-/issues/3) - # That self-dependency isn't caught, so we catch it manually here. - raise ValueError( - f"Detected a cycle in the data flow topology, unable to automate " - f"the execution of non-DAGs: {node.label} appears in its own input." - ) - digraph[node.label] = node_dependencies - - return digraph + _, upstream_most_node = set_run_connections_according_to_linear_dag(self.nodes) + self.starting_nodes = [upstream_most_node] def _build_io( self, diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index 34b85c91..aaea64b7 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -59,9 +59,9 @@ class Function(Node): Further, functions with multiple return branches that return different types or numbers of return values may or may not work smoothly, depending on the details. - Output is updated in the `process_run_result` inside the parent class `finish_run` - call, such that output data gets pushed after the node stops running but before - then `ran` signal fires: run, process and push result, ran. + Output is updated according to `process_run_result` -- which gets invoked by the + post-run callbacks defined in `Node` -- such that run results are used to populate + the output channels. After a node is instantiated, its input can be updated as `*args` and/or `**kwargs` on call. @@ -103,7 +103,7 @@ class Function(Node): run: Parse and process the input, execute the engine, process the results and update the output. disconnect: Disconnect all data and signal IO connections. - update_input: Allows input channels' values to be updated without any running. + set_input_values: Allows input channels' values to be updated without any running. Examples: At the most basic level, to use nodes all we need to do is provide the @@ -173,9 +173,7 @@ class Function(Node): using good variable names and returning those variables instead of using `output_labels`. If we force the node to `run()` (or call it) with bad types, it will raise an - error. - But, if we use the gentler `update()`, it will check types first and simply - return `None` if the input is not all `ready`. + error: >>> from typing import Union >>> >>> def hinted_example( @@ -186,13 +184,17 @@ class Function(Node): ... return p1, m1 >>> >>> plus_minus_1 = Function(hinted_example, x="not an int") - >>> plus_minus_1.update() - >>> plus_minus_1.outputs.to_value_dict() - {'p1': , - 'm1': } + >>> plus_minus_1.run() + ValueError: hinted_example received a run command but is not ready. The node + should be neither running nor failed, and all input values should conform to + type hints: + running: False + failed: False + x ready: False + y ready: True Here, even though all the input has data, the node sees that some of it is the - wrong type and so the automatic updates don't proceed all the way to a run. + wrong type and so (by default) the run raises an error right away. Note that the type hinting doesn't actually prevent us from assigning bad values directly to the channel (although it will, by default, prevent connections _between_ type-hinted channels with incompatible hints), but it _does_ stop the @@ -333,7 +335,7 @@ def __init__( # TODO: Parse output labels from the node function in case output_labels is None self.signals = self._build_signal_channels() - self.update_input(*args, **kwargs) + self.set_input_values(*args, **kwargs) def _get_output_labels(self, output_labels: str | list[str] | tuple[str] | None): """ @@ -516,7 +518,7 @@ def _convert_input_args_and_kwargs_to_input_kwargs(self, *args, **kwargs): return kwargs - def update_input(self, *args, **kwargs) -> None: + def set_input_values(self, *args, **kwargs) -> None: """ Match positional and keyword arguments to input channels and update input values. @@ -527,7 +529,7 @@ def update_input(self, *args, **kwargs) -> None: pairs. """ kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) - return super().update_input(**kwargs) + return super().set_input_values(**kwargs) def __call__(self, *args, **kwargs) -> None: kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index 1fb56756..e81d4975 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -183,7 +183,7 @@ def __init__( self._inputs: Inputs = self._build_inputs() self._outputs: Outputs = self._build_outputs() - self.update_input(**kwargs) + self.set_input_values(**kwargs) def _get_linking_channel( self, diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 1199d380..d2f7a78b 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -16,7 +16,7 @@ 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.topology import set_run_connections_according_to_linear_dag from pyiron_workflow.util import SeabornColors if TYPE_CHECKING: @@ -24,7 +24,7 @@ from pyiron_workflow.channels import Channel from pyiron_workflow.composite import Composite - from pyiron_workflow.io import IO, Inputs, Outputs + from pyiron_workflow.io import Inputs, Outputs def manage_status(node_method): @@ -80,8 +80,11 @@ class Node(HasToDict, ABC): These labels also help to identify nodes in the wider context of (potentially nested) computational graphs. - By default, nodes' signals input comes with `run` and `ran` IO ports which force - the `run()` method and which emit after `finish_run()` is completed, respectfully. + By default, nodes' signals input comes with `run` and `ran` IO ports, which invoke + the `run()` method and emit after running the node, respectfully. + (Whether we get all the way to emitting the `ran` signal depends on how the node + was invoked -- it is possible to computing things with the node without sending + any more signals downstream.) These signal connections can be made manually by reference to the node signals channel, or with the `>` symbol to indicate a flow of execution. This syntactic sugar can be mixed between actual signal channels (output signal > input signal), @@ -101,8 +104,8 @@ class Node(HasToDict, ABC): Nodes have a status, which is currently represented by the `running` and `failed` boolean flag attributes. - Their value is controlled automatically in the defined `run` and `finish_run` - methods. + These are updated automatically when the node's operation is invoked, e.g. with + `run`, `execute`, `pull`, or by calling the node instance. Nodes can be run on the main python process that owns them, or by setting their `executor` attribute to `True`, in which case a @@ -140,6 +143,8 @@ class Node(HasToDict, ABC): owning this, if any. ready (bool): Whether the inputs are all ready and the node is neither already running nor already failed. + run_args (dict): **Abstract** the argmuments to use for actually running the + node. Must be specified in child classes. running (bool): Whether the node has called `run` and has not yet received output from this call. (Default is False.) signals (pyiron_workflow.io.Signals): A container for input and output @@ -152,11 +157,20 @@ class Node(HasToDict, ABC): initialized. Methods: + __call__: Update input values (optional) then run the node (without firing off + .the `ran` signal, so nothing happens farther downstream). disconnect: Remove all connections, including signals. draw: Use graphviz to visualize the node, its IO and, if composite in nature, its internal structure. - on_run: **Abstract.** Do the thing. - run: A wrapper to handle all the infrastructure around executing `on_run`. + execute: Run the node, but right here, right now, and with the input it + currently has. + on_run: **Abstract.** Do the thing. What thing must be specified by child + classes. + pull: Run everything upstream, then run this node (but don't fire off the `ran` + signal, so nothing happens farther downstream). + run: Run the node function from `on_run`. Handles status, whether to run on an + executor, firing the `ran` signal, and callbacks (if an executor is used). + set_input_values: Allows input channels' values to be updated without any running. """ def __init__( @@ -182,8 +196,6 @@ def __init__( parent.add(self) self.running = False self.failed = False - # TODO: Move from a traditional "sever" to a tinybase "executor" - # TODO: Provide support for actually computing stuff with the executor self.signals = self._build_signal_channels() self._working_directory = None self.executor = False @@ -231,74 +243,68 @@ 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): + def run( + self, + first_fetch_input: bool = True, + then_emit_output_signals: bool = True, + force_local_execution: bool = False, + check_readiness: bool = True, + ): """ 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. + any other nodes to run) and use it to perform the node's operation. After, + emit all output signals. 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.update_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.update_input() - return self._run(finished_callback=self.finish_run) - - def update_input(self, **kwargs) -> None: - """ - 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. - - 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. - """ - self.inputs.fetch() - 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" - ) + first_fetch_input (bool): Whether to first update inputs with the + highest-priority connections holding data. (Default is True.) + then_emit_output_signals (bool): Whether to fire off all output signals + (e.g. `ran`) afterwards. (Default is True.) + force_local_execution (bool): Whether to ignore any executor settings and + force the computation to run locally. (Default is False.) + check_readiness (bool): Whether to raise an exception if the node is not + `ready` to run after fetching new input. (Default is True.) + + Returns: + (Any | Future): The result of running the node, or a futures object (if + running on an executor). + """ + if first_fetch_input: + self.inputs.fetch() + if check_readiness and not self.ready: + input_readiness = "\n".join( + [f"{k} ready: {v.ready}" for k, v in self.inputs.items()] + ) + raise ValueError( + f"{self.label} received a run command but is not ready. The node " + f"should be neither running nor failed, and all input values should" + f" conform to type hints:\n" + f"running: {self.running}\n" + f"failed: {self.failed}\n" + input_readiness + ) + return self._run( + finished_callback=self._finish_run_and_emit_ran + if then_emit_output_signals + else self._finish_run, + force_local_execution=force_local_execution, + ) @manage_status - def _run(self, finished_callback: callable) -> Any | tuple | Future: + def _run( + self, + finished_callback: callable, + force_local_execution: bool, + ) -> 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 not self.executor: + if force_local_execution or not self.executor: + # Run locally run_output = self.on_run(**self.run_args) return finished_callback(run_output) else: @@ -309,7 +315,7 @@ def _run(self, finished_callback: callable) -> Any | tuple | Future: self.future.add_done_callback(finished_callback) return self.future - def finish_run(self, run_output: tuple | Future) -> Any | tuple: + def _finish_run(self, run_output: tuple | Future) -> Any | tuple: """ Switch the node status, then process and return the run result. @@ -326,29 +332,118 @@ def finish_run(self, run_output: tuple | Future) -> Any | tuple: 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) + 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 execute(self): + """ + Run the node with whatever input it currently has, run it on this python + process, and don't emit the `ran` signal afterwards. + + Intended to be useful for debugging by just forcing the node to do its thing + right here, right now, and as-is. + """ + return self.run( + first_fetch_input=False, + then_emit_output_signals=False, + force_local_execution=True, + check_readiness=False, + ) + + def pull(self): + """ + Use topological analysis to build a tree of all upstream dependencies; run them + first, then run this node to get an up-to-date result. Does not trigger any + downstream executions. + """ + label_map = {} + nodes = {} + for node in self.get_nodes_in_data_tree(): + modified_label = node.label + str(id(node)) + label_map[modified_label] = node.label + node.label = modified_label # Ensure each node has a unique label + # This is necessary when the nodes do not have a workflow and may thus have + # arbitrary labels. + # This is pretty ugly; it would be nice to not depend so heavily on labels. + # Maybe we could switch a bunch of stuff to rely on the unique ID? + nodes[modified_label] = node + disconnected_pairs, starter = set_run_connections_according_to_linear_dag(nodes) + try: + self.signals.disconnect_run() # Don't let anything upstream trigger this + starter.run() # Now push from the top + return self.run() # Finally, run here and return the result + # Emitting won't matter since we already disconnected this one + finally: + # No matter what, restore the original connections and labels afterwards + for modified_label, node in nodes.items(): + node.label = label_map[modified_label] + node.signals.disconnect_run() + for c1, c2 in disconnected_pairs: + c1.connect(c2) + + def get_nodes_in_data_tree(self) -> set[Node]: + """ + Get a set of all nodes from this one and upstream through data connections. + """ + nodes = set([self]) + for channel in self.inputs: + for connection in channel.connections: + nodes = nodes.union(connection.node.get_nodes_in_data_tree()) + return nodes + + def __call__(self, **kwargs) -> None: + """ + Update the input, then run without firing the `ran` signal. + + Note that since input fetching happens _after_ the input values are updated, + if there is a connected data value it will get used instead of what is specified + here. If you really want to set a particular state and then run this can be + accomplished with `.inputs.fetch()` then `.set_input_values(...)` then + `.execute()` (or `.run(...)` with the flags you want). + + Args: + **kwargs: Keyword arguments matching input channel labels; used to update + the input before running. + """ + self.set_input_values(**kwargs) + return self.run() + + def set_input_values(self, **kwargs) -> None: + """ + Match keywords to input channels and update their values. + + Throws a warning if a keyword is provided that cannot be found among the input + keys. + + Args: + **kwargs: input key - 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 _build_signal_channels(self) -> Signals: signals = Signals() signals.input.run = InputSignal("run", self, self.run) signals.output.ran = OutputSignal("ran", self) return signals - def update(self) -> Any | tuple | Future | None: - if self.ready: - return self.run() - @property def working_directory(self): if self._working_directory is None: @@ -389,10 +484,6 @@ def fully_connected(self): and self.signals.fully_connected ) - def __call__(self, **kwargs) -> None: - self.update_input(**kwargs) - return self.run() - @property def color(self) -> str: """A hex code color for use in drawing.""" diff --git a/pyiron_workflow/topology.py b/pyiron_workflow/topology.py new file mode 100644 index 00000000..06d53631 --- /dev/null +++ b/pyiron_workflow/topology.py @@ -0,0 +1,156 @@ +""" +A submodule for getting our node classes talking nicely with an external tool for +topological analysis. Such analyses are useful for automating execution flows based on +data flow dependencies. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from toposort import toposort_flatten, CircularDependencyError + +if TYPE_CHECKING: + from pyiron_workflow.channels import SignalChannel + from pyiron_workflow.node import Node + + +def nodes_to_data_digraph(nodes: dict[str, Node]) -> dict[str, set[str]]: + """ + Maps a set of nodes to a digraph of their data dependency in the format of label + keys and set of label values for upstream nodes. + + Args: + nodes (dict[str, Node]): A label-keyed dictionary of nodes to convert into a + string-based dictionary of digraph connections based on data flow. + + Returns: + dict[str, set[str]]: A dictionary of nodes and the nodes they depend on for + data. + + Raises: + ValueError: When a node appears in its own input. + ValueError: If the nodes do not all have the same parent. + ValueError: If one of the nodes has an upstream data connection whose node has + a different parent. + """ + digraph = {} + + parent = next(iter(nodes.values())).parent # Just grab any one + if not all(n.parent is parent for n in nodes.values()): + raise ValueError( + "Nodes in a data digraph must all be siblings -- i.e. have the same " + "`parent` attribute." + ) + + for node in nodes.values(): + node_dependencies = [] + for channel in node.inputs: + locally_scoped_dependencies = [] + for upstream in channel.connections: + try: + upstream_node = nodes[upstream.node.label] + except KeyError as e: + raise KeyError( + f"The {channel.label} channel of {node.label} has a connection " + f"to {upstream.label} channel of {upstream.node.label}, but " + f"{upstream.node.label} was not found among nodes. All nodes " + f"in the data flow dependency tree must be included." + ) + if upstream_node is not upstream.node: + raise ValueError( + f"The {channel.label} channel of {node.label} has a connection " + f"to {upstream.label} channel of {upstream.node.label}, but " + f"that channel's node is not the same as the nodes passed " + f"here. All nodes in the data flow dependency tree must be " + f"included." + ) + locally_scoped_dependencies.append(upstream.node.label) + node_dependencies.extend(locally_scoped_dependencies) + node_dependencies = set(node_dependencies) + if node.label in node_dependencies: + # the toposort library has a + # [known issue](https://gitlab.com/ericvsmith/toposort/-/issues/3) + # That self-dependency isn't caught, so we catch it manually here. + raise ValueError( + f"Detected a cycle in the data flow topology, unable to automate " + f"the execution of non-DAGs: {node.label} appears in its own input." + ) + digraph[node.label] = node_dependencies + + return digraph + + +def nodes_to_execution_order(nodes: dict[str, Node]) -> list[str]: + """ + Given a set of nodes that all have the same parent, returns a list of corresponding + node labels giving an execution order that guarantees the executing node always has + data from all its upstream nodes. + + Args: + nodes (dict[str, Node]): A label-keyed dictionary of nodes from whom to build + an execution order based on topological analysis of data flow. + + Returns: + (list[str]): The labels in safe execution order. + + Raises: + CircularDependencyError: If the data dependency is not a Directed Acyclic Graph + """ + try: + # Topological sorting ensures that all input dependencies have been + # executed before the node depending on them gets run + # The flattened part is just that we don't care about topological + # generations that are mutually independent (inefficient but easier for now) + execution_order = toposort_flatten(nodes_to_data_digraph(nodes)) + except CircularDependencyError as e: + raise ValueError( + f"Detected a cycle in the data flow topology, unable to automate the " + f"execution of non-DAGs: cycles found among {e.data}" + ) + return execution_order + + +def set_run_connections_according_to_linear_dag( + nodes: dict[str, Node] +) -> tuple[list[tuple[SignalChannel, SignalChannel]], Node]: + """ + Given a set of nodes that all have the same parent, have no upstream data + connections outside the nodes provided, and have acyclic data flow, disconnects all + their `run` and `ran` signals, then sets these signals to a linear execution that + guarantees downstream nodes are always executed after upstream nodes. Returns one + of the upstream-most nodes. + + In the event an exception is encountered, any disconnected connections are repaired + before it is raised. + + Args: + nodes (dict[str, Node]): A dictionary of node labels and the node the label is + from, whose connections will be set according to data flow. + + Returns: + (list[tuple[SignalChannel, SignalChannel]]): Any `run`/`ran` pairs that were + disconnected. + (Node): The 0th node in the execution order, i.e. on that has no + dependencies. + """ + disconnected_pairs = [] + for node in nodes.values(): + disconnected_pairs.extend(node.signals.disconnect_run()) + disconnected_pairs.extend(node.signals.output.ran.disconnect_all()) + + try: + # This is the most primitive sort of topological exploitation we can do + # It is not efficient if the nodes have executors and can run in parallel + execution_order = nodes_to_execution_order(nodes) + + for i, label in enumerate(execution_order[:-1]): + next_node = execution_order[i + 1] + nodes[label] > nodes[next_node] + + return disconnected_pairs, nodes[execution_order[0]] + except Exception as e: + # Restore whatever you broke + for c1, c2 in disconnected_pairs: + c1.connect(c2) + raise e diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 5817fa7e..36eee604 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -203,7 +203,13 @@ def inputs(self) -> Inputs: def outputs(self) -> Outputs: return self._build_outputs() - def run(self): + def run( + self, + first_fetch_input: bool = True, + then_emit_output_signals: bool = True, + force_local_execution: bool = False, + check_readiness: bool = True, + ): if self.automate_execution: self.set_run_signals_to_dag_execution() return super().run() diff --git a/tests/integration/test_pull.py b/tests/integration/test_pull.py new file mode 100644 index 00000000..75e85aca --- /dev/null +++ b/tests/integration/test_pull.py @@ -0,0 +1,84 @@ +import unittest + +from pyiron_workflow.workflow import Workflow + + +class TestPullingOutput(unittest.TestCase): + def test_without_workflow(self): + from pyiron_workflow import Workflow + + @Workflow.wrap_as.single_value_node("sum") + def x_plus_y(x: int = 0, y: int = 0) -> int: + return x + y + + node = x_plus_y( + x=x_plus_y(0, 1), + y=x_plus_y(2, 3) + ) + self.assertEqual(6, node.pull()) + + for n in [ + node, + node.inputs.x.connections[0].node, + node.inputs.y.connections[0].node, + ]: + self.assertFalse( + n.signals.connected, + msg="Connections should be unwound after the pull is done" + ) + self.assertEqual( + "x_plus_y", + n.label, + msg="Original labels should be restored after the pull is done" + ) + + def test_pulling_from_inside_a_macro(self): + @Workflow.wrap_as.single_value_node("sum") + def x_plus_y(x: int = 0, y: int = 0) -> int: + # print("EXECUTING") + return x + y + + @Workflow.wrap_as.macro_node() + def b2_leaves_a1_alone(macro): + macro.a1 = x_plus_y(0, 0) + macro.a2 = x_plus_y(0, 1) + macro.b1 = x_plus_y(macro.a1, macro.a2) + macro.b2 = x_plus_y(macro.a2, 10) + + wf = Workflow("demo") + wf.upstream = x_plus_y() + wf.macro = b2_leaves_a1_alone(a2__x=wf.upstream) + + # Pulling b1 -- executes a1, a2, b2 + self.assertEqual(1, wf.macro.b1.pull()) + # >>> EXECUTING + # >>> EXECUTING + # >>> EXECUTING + # >>> 1 + + # Pulling b2 -- executes a2, a1 + self.assertEqual(11, wf.macro.b2.pull()) + # >>> EXECUTING + # >>> EXECUTING + # >>> 11 + + # Updated inputs get reflected in the pull + wf.macro.set_input_values(a1__x=100, a2__x=-100) + self.assertEqual(-89, wf.macro.b2.pull()) + # >>> EXECUTING + # >>> EXECUTING + # >>> -89 + + # Connections are restored after a pull + # Crazy negative value of a2 gets written over by pulling in the upstream + # connection value + # Running wf -- executes upstream, macro (is silent), a1, a2, b1, b2 + out = wf() + self.assertEqual(101, out.macro__b1__sum) + self.assertEqual(11, out.macro__b2__sum) + # >>> EXECUTING + # >>> EXECUTING + # >>> EXECUTING + # >>> EXECUTING + # >>> EXECUTING + # >>> {'macro__b1__sum': 101, 'macro__b2__sum': 11} \ No newline at end of file diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 4f295899..28b8fbb8 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -286,7 +286,7 @@ def with_self(self, x: float) -> float: msg="Expected 'self' to be filtered out of node input, but found it in the " "input labels" ) - node.inputs.x = 1 + node.inputs.x = 1.0 node.run() self.assertEqual( node.outputs.output.value, @@ -489,7 +489,12 @@ def all_floats(x=1.1, y=1.1, z=1.1, omega=NotData, extra_there=None) -> float: ref = reference() floats = all_floats() ref() - floats() + floats.run( + check_readiness=False, + # We force-skip the readiness check since we are explicitly _trying_ to + # have one of the inputs be `NotData` -- a value which triggers the channel + # to be "not ready" + ) ref._copy_values(floats) self.assertEqual(