From 492d669228d672129d3234767ec8917eedefa73d Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 31 Oct 2023 12:25:03 -0700 Subject: [PATCH 01/35] Separate the pull call from running upstream nodes --- pyiron_workflow/node.py | 69 +++++++++++++++++++++++++++++++------ pyiron_workflow/workflow.py | 11 +++++- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index d2f7a78b..57904818 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -245,20 +245,28 @@ def process_run_result(self, run_output): def run( self, + run_data_tree: bool = False, + run_parent_trees_too: bool = False, 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. After, - emit all output signals. + The master method for running in a variety of ways. + By default, whatever data is currently available in upstream nodes will be + fetched, if the input all conforms to type hints then this node will be run + (perhaps using an executor), and finally the `ran` signal will be emitted to + trigger downstream runs. If executor information is specified, execution happens on that process, a callback is registered, and futures object is returned. Args: + run_data_tree (bool): Whether to first run all upstream nodes in the data + graph. (Default is False.) + run_parent_trees_too (bool): Whether to recursively run the data tree in + parent nodes (if any). (Default is False.) 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 @@ -271,9 +279,17 @@ def run( Returns: (Any | Future): The result of running the node, or a futures object (if running on an executor). + + Note: + Running data trees is a pull-based paradigm and only compatible with graphs + whose data forms a directed acyclic graph (DAG). """ + if run_data_tree: + self.run_data_tree(run_parent_trees_too=run_parent_trees_too) + 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()] @@ -285,6 +301,7 @@ def run( 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 @@ -354,18 +371,27 @@ def execute(self): right here, right now, and as-is. """ return self.run( + run_data_tree=False, + run_parent_trees_too=False, first_fetch_input=False, then_emit_output_signals=False, force_local_execution=True, check_readiness=False, ) - def pull(self): + def run_data_tree(self, run_parent_trees_too=False) -> None: """ - 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. + Use topological analysis to build a tree of all upstream dependencies and run + them. + + Args: + run_parent_trees_too (bool): First, call the same method on this node's + parent (if one exists), and recursively up the parentage tree. (Default + is False, only run nodes in this scope, i.e. sharing the same parent.) """ + if run_parent_trees_too and self.parent is not None: + self.parent.run_data_tree(pull_from_parents=True) + label_map = {} nodes = {} for node in self.get_nodes_in_data_tree(): @@ -377,12 +403,23 @@ def pull(self): # 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: + disconnected_pairs, starter = set_run_connections_according_to_linear_dag( + nodes + ) + except Exception as e: + # If the dag setup fails it will repair any connections it breaks before + # raising the error, but we still need to repair our label changes + for modified_label, node in nodes.items(): + node.label = label_map[modified_label] + raise e + + self.signals.disconnect_run() + # Don't let anything upstream trigger this node + 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(): @@ -391,6 +428,16 @@ def pull(self): for c1, c2 in disconnected_pairs: c1.connect(c2) + def pull(self, run_parent_trees_too=False): + return self.run( + run_data_tree=True, + run_parent_trees_too=run_parent_trees_too, + first_fetch_input=True, + then_emit_output_signals=False, + force_local_execution=False, + check_readiness=True, + ) + def get_nodes_in_data_tree(self) -> set[Node]: """ Get a set of all nodes from this one and upstream through data connections. diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 36eee604..169667b5 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -210,9 +210,18 @@ def run( force_local_execution: bool = False, check_readiness: bool = True, ): + # Note: Workflows may not have parents, so we don't need to worry about running + # their data trees first, hence the change in signature from Node.run if self.automate_execution: self.set_run_signals_to_dag_execution() - return super().run() + return super().run( + run_data_tree=False, + run_parent_trees_too=False, + first_fetch_input=first_fetch_input, + then_emit_output_signals=then_emit_output_signals, + force_local_execution=force_local_execution, + check_readiness=check_readiness, + ) def to_node(self): """ From da90578ac85e076d9c3d3d30a6e9a5b4906c0bc2 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 31 Oct 2023 12:27:13 -0700 Subject: [PATCH 02/35] Prohibit workflows from pulling Since they are required to be parent-most --- pyiron_workflow/workflow.py | 12 ++++++++++++ tests/unit/test_workflow.py | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 169667b5..faac700b 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -223,6 +223,18 @@ def run( check_readiness=check_readiness, ) + def pull(self, run_parent_trees_too=False): + raise NotImplementedError( + f"{self.__class__.__name__} must be a parent-most node, and therefore has " + f"no one to pull data from." + ) + + def run_data_tree(self, run_parent_trees_too=False) -> None: + raise NotImplementedError( + f"{self.__class__.__name__} must be a parent-most node, and therefore has " + f"no upstream data tree to run." + ) + def to_node(self): """ Export the workflow to a macro node, with the currently exposed IO mapped to diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index fc7b8775..8cddb588 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -462,6 +462,19 @@ def test_io_label_maps_are_bijective(self): with self.assertRaises(ValueDuplicationError): wf.inputs_map["foo2__x"] = "x1" + def test_pull(self): + wf = Workflow("parent_most") + with self.assertRaises( + NotImplementedError, + msg="Workflows are a parent-most object" + ): + wf.pull() + with self.assertRaises( + NotImplementedError, + msg="Workflows are a parent-most object" + ): + wf.run_data_tree() + if __name__ == '__main__': unittest.main() From 95b1d7ab900262f2fceaaf1b17eb88e50af68faa Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 31 Oct 2023 12:30:51 -0700 Subject: [PATCH 03/35] Refactor: slide So that called methods appear in the order they're called --- pyiron_workflow/node.py | 98 ++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 57904818..528be9f9 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -309,6 +309,55 @@ def run( force_local_execution=force_local_execution, ) + def run_data_tree(self, run_parent_trees_too=False) -> None: + """ + Use topological analysis to build a tree of all upstream dependencies and run + them. + + Args: + run_parent_trees_too (bool): First, call the same method on this node's + parent (if one exists), and recursively up the parentage tree. (Default + is False, only run nodes in this scope, i.e. sharing the same parent.) + """ + if run_parent_trees_too and self.parent is not None: + self.parent.run_data_tree(pull_from_parents=True) + + 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 + + try: + disconnected_pairs, starter = set_run_connections_according_to_linear_dag( + nodes + ) + except Exception as e: + # If the dag setup fails it will repair any connections it breaks before + # raising the error, but we still need to repair our label changes + for modified_label, node in nodes.items(): + node.label = label_map[modified_label] + raise e + + self.signals.disconnect_run() + # Don't let anything upstream trigger this node + + try: + starter.run() # Now push from the top + 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) + @manage_status def _run( self, @@ -379,55 +428,6 @@ def execute(self): check_readiness=False, ) - def run_data_tree(self, run_parent_trees_too=False) -> None: - """ - Use topological analysis to build a tree of all upstream dependencies and run - them. - - Args: - run_parent_trees_too (bool): First, call the same method on this node's - parent (if one exists), and recursively up the parentage tree. (Default - is False, only run nodes in this scope, i.e. sharing the same parent.) - """ - if run_parent_trees_too and self.parent is not None: - self.parent.run_data_tree(pull_from_parents=True) - - 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 - - try: - disconnected_pairs, starter = set_run_connections_according_to_linear_dag( - nodes - ) - except Exception as e: - # If the dag setup fails it will repair any connections it breaks before - # raising the error, but we still need to repair our label changes - for modified_label, node in nodes.items(): - node.label = label_map[modified_label] - raise e - - self.signals.disconnect_run() - # Don't let anything upstream trigger this node - - try: - starter.run() # Now push from the top - 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 pull(self, run_parent_trees_too=False): return self.run( run_data_tree=True, From 857d1d244bf06f348e2da1e57618aef0abdcb2ab Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 31 Oct 2023 12:31:14 -0700 Subject: [PATCH 04/35] Refactor: slide So that called methods appear in the order they're called --- pyiron_workflow/node.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 528be9f9..2be923de 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -358,6 +358,16 @@ def run_data_tree(self, run_parent_trees_too=False) -> None: 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 + @manage_status def _run( self, @@ -438,16 +448,6 @@ def pull(self, run_parent_trees_too=False): check_readiness=True, ) - 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. From b4c1338bb0faebe58bc2350d9e920797c4a8c884 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 31 Oct 2023 12:35:27 -0700 Subject: [PATCH 05/35] Refactor: rename variable --- pyiron_workflow/node.py | 10 +++++----- pyiron_workflow/workflow.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 2be923de..5720452b 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -247,7 +247,7 @@ def run( self, run_data_tree: bool = False, run_parent_trees_too: bool = False, - first_fetch_input: bool = True, + fetch_input: bool = True, then_emit_output_signals: bool = True, force_local_execution: bool = False, check_readiness: bool = True, @@ -267,7 +267,7 @@ def run( graph. (Default is False.) run_parent_trees_too (bool): Whether to recursively run the data tree in parent nodes (if any). (Default is False.) - first_fetch_input (bool): Whether to first update inputs with the + 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.) @@ -287,7 +287,7 @@ def run( if run_data_tree: self.run_data_tree(run_parent_trees_too=run_parent_trees_too) - if first_fetch_input: + if fetch_input: self.inputs.fetch() if check_readiness and not self.ready: @@ -432,7 +432,7 @@ def execute(self): return self.run( run_data_tree=False, run_parent_trees_too=False, - first_fetch_input=False, + fetch_input=False, then_emit_output_signals=False, force_local_execution=True, check_readiness=False, @@ -442,7 +442,7 @@ def pull(self, run_parent_trees_too=False): return self.run( run_data_tree=True, run_parent_trees_too=run_parent_trees_too, - first_fetch_input=True, + fetch_input=True, then_emit_output_signals=False, force_local_execution=False, check_readiness=True, diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index faac700b..3ad92a4f 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -205,7 +205,7 @@ def outputs(self) -> Outputs: def run( self, - first_fetch_input: bool = True, + fetch_input: bool = True, then_emit_output_signals: bool = True, force_local_execution: bool = False, check_readiness: bool = True, @@ -217,7 +217,7 @@ def run( return super().run( run_data_tree=False, run_parent_trees_too=False, - first_fetch_input=first_fetch_input, + fetch_input=fetch_input, then_emit_output_signals=then_emit_output_signals, force_local_execution=force_local_execution, check_readiness=check_readiness, From 7b5576c11f1093907b6b8695dbfdd1bbc0edfc40 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 31 Oct 2023 12:36:31 -0700 Subject: [PATCH 06/35] Refactor: rename variable --- pyiron_workflow/node.py | 12 ++++++------ pyiron_workflow/workflow.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 5720452b..b5d867e7 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -248,7 +248,7 @@ def run( run_data_tree: bool = False, run_parent_trees_too: bool = False, fetch_input: bool = True, - then_emit_output_signals: bool = True, + emit_ran_signal: bool = True, force_local_execution: bool = False, check_readiness: bool = True, ): @@ -269,8 +269,8 @@ def run( parent nodes (if any). (Default is False.) 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.) + emit_ran_signal (bool): Whether to fire off all the output `ran` signal + 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 @@ -304,7 +304,7 @@ def run( return self._run( finished_callback=self._finish_run_and_emit_ran - if then_emit_output_signals + if emit_ran_signal else self._finish_run, force_local_execution=force_local_execution, ) @@ -433,7 +433,7 @@ def execute(self): run_data_tree=False, run_parent_trees_too=False, fetch_input=False, - then_emit_output_signals=False, + emit_ran_signal=False, force_local_execution=True, check_readiness=False, ) @@ -443,7 +443,7 @@ def pull(self, run_parent_trees_too=False): run_data_tree=True, run_parent_trees_too=run_parent_trees_too, fetch_input=True, - then_emit_output_signals=False, + emit_ran_signal=False, force_local_execution=False, check_readiness=True, ) diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 3ad92a4f..cf3e0894 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -206,7 +206,7 @@ def outputs(self) -> Outputs: def run( self, fetch_input: bool = True, - then_emit_output_signals: bool = True, + emit_ran_signal: bool = True, force_local_execution: bool = False, check_readiness: bool = True, ): @@ -218,7 +218,7 @@ def run( run_data_tree=False, run_parent_trees_too=False, fetch_input=fetch_input, - then_emit_output_signals=then_emit_output_signals, + emit_ran_signal=emit_ran_signal, force_local_execution=force_local_execution, check_readiness=check_readiness, ) From 79ef2c81126f103649a36ca114f09474aeca9442 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 31 Oct 2023 12:41:20 -0700 Subject: [PATCH 07/35] Refactor: reorder signature So that the boolean flags appear in the order of execution flow --- pyiron_workflow/node.py | 20 ++++++++++---------- pyiron_workflow/workflow.py | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index b5d867e7..a190e812 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -248,9 +248,9 @@ def run( run_data_tree: bool = False, run_parent_trees_too: bool = False, fetch_input: bool = True, - emit_ran_signal: bool = True, - force_local_execution: bool = False, check_readiness: bool = True, + force_local_execution: bool = False, + emit_ran_signal: bool = True, ): """ The master method for running in a variety of ways. @@ -269,12 +269,12 @@ def run( parent nodes (if any). (Default is False.) fetch_input (bool): Whether to first update inputs with the highest-priority connections holding data. (Default is True.) - emit_ran_signal (bool): Whether to fire off all the output `ran` signal - 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.) + force_local_execution (bool): Whether to ignore any executor settings and + force the computation to run locally. (Default is False.) + emit_ran_signal (bool): Whether to fire off all the output `ran` signal + afterwards. (Default is True.) Returns: (Any | Future): The result of running the node, or a futures object (if @@ -433,9 +433,9 @@ def execute(self): run_data_tree=False, run_parent_trees_too=False, fetch_input=False, - emit_ran_signal=False, - force_local_execution=True, check_readiness=False, + force_local_execution=True, + emit_ran_signal=False, ) def pull(self, run_parent_trees_too=False): @@ -443,9 +443,9 @@ def pull(self, run_parent_trees_too=False): run_data_tree=True, run_parent_trees_too=run_parent_trees_too, fetch_input=True, - emit_ran_signal=False, - force_local_execution=False, check_readiness=True, + force_local_execution=False, + emit_ran_signal=False, ) def __call__(self, **kwargs) -> None: diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index cf3e0894..9fd874a3 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -206,9 +206,9 @@ def outputs(self) -> Outputs: def run( self, fetch_input: bool = True, - emit_ran_signal: bool = True, - force_local_execution: bool = False, check_readiness: bool = True, + force_local_execution: bool = False, + emit_ran_signal: bool = True, ): # Note: Workflows may not have parents, so we don't need to worry about running # their data trees first, hence the change in signature from Node.run @@ -218,9 +218,9 @@ def run( run_data_tree=False, run_parent_trees_too=False, fetch_input=fetch_input, - emit_ran_signal=emit_ran_signal, - force_local_execution=force_local_execution, check_readiness=check_readiness, + force_local_execution=force_local_execution, + emit_ran_signal=emit_ran_signal, ) def pull(self, run_parent_trees_too=False): From d147c9c53b063f57b03f4850e124f414034e7b73 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 31 Oct 2023 12:42:02 -0700 Subject: [PATCH 08/35] Update execute docstring --- pyiron_workflow/node.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index a190e812..cac0fcde 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -423,6 +423,8 @@ def _finish_run_and_emit_ran(self, run_output: tuple | Future) -> Any | tuple: def execute(self): """ + A shortcut for `run` with particular flags. + Run the node with whatever input it currently has, run it on this python process, and don't emit the `ran` signal afterwards. From c5fc0c53ab8c47f077bbf0b3dd282e1e052ea4bf Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 31 Oct 2023 12:45:32 -0700 Subject: [PATCH 09/35] Add a docstring to pull --- pyiron_workflow/node.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index cac0fcde..d7618c85 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -441,6 +441,18 @@ def execute(self): ) def pull(self, run_parent_trees_too=False): + """ + A shortcut for `run` with particular flags. + + Runs nodes upstream in the data graph, then runs this node without triggering + any downstream runs. By default only runs sibling nodes, but can optionally + require the parent node to pull in its own upstream runs (this is recursive + up to the parent-most object). + + Args: + run_parent_trees_too (bool): Whether to (recursively) require the parent to + first pull. + """ return self.run( run_data_tree=True, run_parent_trees_too=run_parent_trees_too, From 45dbe96576257f888e6da424ce49b04a06db1f53 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 31 Oct 2023 12:49:57 -0700 Subject: [PATCH 10/35] Be more consistent in restricting Workflow.run signature --- pyiron_workflow/workflow.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 9fd874a3..2ae246bd 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -205,22 +205,21 @@ def outputs(self) -> Outputs: def run( self, - fetch_input: bool = True, check_readiness: bool = True, force_local_execution: bool = False, - emit_ran_signal: bool = True, ): - # Note: Workflows may not have parents, so we don't need to worry about running - # their data trees first, hence the change in signature from Node.run + # Note: Workflows may have neither parents nor siblings, so we don't need to + # worry about running their data trees first, fetching their input, nor firing + # their `ran` signal, hence the change in signature from Node.run if self.automate_execution: self.set_run_signals_to_dag_execution() return super().run( run_data_tree=False, run_parent_trees_too=False, - fetch_input=fetch_input, + fetch_input=False, check_readiness=check_readiness, force_local_execution=force_local_execution, - emit_ran_signal=emit_ran_signal, + emit_ran_signal=False, ) def pull(self, run_parent_trees_too=False): From e8d064d2c38ae428aea28c6348e976a1136e1d3e Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 31 Oct 2023 12:50:05 -0700 Subject: [PATCH 11/35] Be gentler when a workflow pulls --- pyiron_workflow/workflow.py | 6 ++---- tests/unit/test_workflow.py | 7 +------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 2ae246bd..4b07b3f6 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -223,10 +223,8 @@ def run( ) def pull(self, run_parent_trees_too=False): - raise NotImplementedError( - f"{self.__class__.__name__} must be a parent-most node, and therefore has " - f"no one to pull data from." - ) + """Workflows are a parent-most object, so this simply runs without pulling.""" + return self.run() def run_data_tree(self, run_parent_trees_too=False) -> None: raise NotImplementedError( diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index 8cddb588..345531c9 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -462,13 +462,8 @@ def test_io_label_maps_are_bijective(self): with self.assertRaises(ValueDuplicationError): wf.inputs_map["foo2__x"] = "x1" - def test_pull(self): + def test_run_data_tree(self): wf = Workflow("parent_most") - with self.assertRaises( - NotImplementedError, - msg="Workflows are a parent-most object" - ): - wf.pull() with self.assertRaises( NotImplementedError, msg="Workflows are a parent-most object" From b762135636b94741ad8a4e2b710633520809eee9 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 31 Oct 2023 13:03:01 -0700 Subject: [PATCH 12/35] Let all the different runners update input with kwargs --- pyiron_workflow/function.py | 8 ++++++++ pyiron_workflow/node.py | 36 +++++++++++++++++++----------------- pyiron_workflow/workflow.py | 6 ++++-- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index 058f79e7..e9a6ce12 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -532,6 +532,14 @@ def set_input_values(self, *args, **kwargs) -> None: kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) return super().set_input_values(**kwargs) + def execute(self, *args, **kwargs): + kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) + return super().execute(**kwargs) + + def pull(self, *args, run_parent_trees_too=False, **kwargs): + kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) + return super().pull(run_parent_trees_too=run_parent_trees_too, **kwargs) + def __call__(self, *args, **kwargs) -> None: kwargs = self._convert_input_args_and_kwargs_to_input_kwargs(*args, **kwargs) return super().__call__(**kwargs) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index d7618c85..c07f954a 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -251,6 +251,7 @@ def run( check_readiness: bool = True, force_local_execution: bool = False, emit_ran_signal: bool = True, + **kwargs, ): """ The master method for running in a variety of ways. @@ -275,6 +276,8 @@ def run( force the computation to run locally. (Default is False.) emit_ran_signal (bool): Whether to fire off all the output `ran` signal afterwards. (Default is True.) + **kwargs: Keyword arguments matching input channel labels; used to update + the input channel values before running anything. Returns: (Any | Future): The result of running the node, or a futures object (if @@ -283,7 +286,15 @@ def run( Note: Running data trees is a pull-based paradigm and only compatible with graphs whose data forms a directed acyclic graph (DAG). + + Note: + Kwargs updating input channel values happens _first_ and will get + overwritten by any subsequent graph-based data manipulation. If you really + want to execute the node with a particular set of input, set it all manually + and use `execute` (or `run` with carefully chosen flags). """ + self.set_input_values(**kwargs) + if run_data_tree: self.run_data_tree(run_parent_trees_too=run_parent_trees_too) @@ -421,12 +432,12 @@ def _finish_run_and_emit_ran(self, run_output: tuple | Future) -> Any | tuple: """ ) - def execute(self): + def execute(self, **kwargs): """ A shortcut for `run` with particular flags. - Run the node with whatever input it currently has, run it on this python - process, and don't emit the `ran` signal afterwards. + Run the node with whatever input it currently has (or is given as kwargs here), + 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. @@ -438,9 +449,10 @@ def execute(self): check_readiness=False, force_local_execution=True, emit_ran_signal=False, + **kwargs, ) - def pull(self, run_parent_trees_too=False): + def pull(self, run_parent_trees_too=False, **kwargs): """ A shortcut for `run` with particular flags. @@ -460,24 +472,14 @@ def pull(self, run_parent_trees_too=False): check_readiness=True, force_local_execution=False, emit_ran_signal=False, + **kwargs, ) 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. + A shortcut for `run`. """ - self.set_input_values(**kwargs) - return self.run() + return self.run(**kwargs) def set_input_values(self, **kwargs) -> None: """ diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 4b07b3f6..176fb653 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -207,6 +207,7 @@ def run( self, check_readiness: bool = True, force_local_execution: bool = False, + **kwargs, ): # Note: Workflows may have neither parents nor siblings, so we don't need to # worry about running their data trees first, fetching their input, nor firing @@ -220,11 +221,12 @@ def run( check_readiness=check_readiness, force_local_execution=force_local_execution, emit_ran_signal=False, + **kwargs, ) - def pull(self, run_parent_trees_too=False): + def pull(self, run_parent_trees_too=False, **kwargs): """Workflows are a parent-most object, so this simply runs without pulling.""" - return self.run() + return self.run(**kwargs) def run_data_tree(self, run_parent_trees_too=False) -> None: raise NotImplementedError( From f68aaabbf7d5158d178ee54e0a3d45837978043b Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 31 Oct 2023 13:25:43 -0700 Subject: [PATCH 13/35] Make __call__ be the most aggressive thing it can be --- pyiron_workflow/node.py | 7 +++++-- tests/unit/test_macro.py | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index c07f954a..80616f1e 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -360,7 +360,10 @@ def run_data_tree(self, run_parent_trees_too=False) -> None: # Don't let anything upstream trigger this node try: - starter.run() # Now push from the top + # If you're the only one in the data tree, there's nothing upstream to run + # Otherwise... + if starter is not self: + starter.run() # Now push from the top finally: # No matter what, restore the original connections and labels afterwards for modified_label, node in nodes.items(): @@ -479,7 +482,7 @@ def __call__(self, **kwargs) -> None: """ A shortcut for `run`. """ - return self.run(**kwargs) + return self.pull(run_parent_tree_too=True, **kwargs) def set_input_values(self, **kwargs) -> None: """ diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 73bae0f5..3f28a4ff 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -375,11 +375,9 @@ def test_macro_connections_after_replace(self): # 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) + macro = Macro(add_three_macro, one__x=0) 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() + downstream.pull() self.assertEqual( 0 + (1 + 1 + 1) + 1, downstream.outputs.result.value, @@ -392,7 +390,7 @@ def add_two(x): compatible_replacement = SingleValue(add_two) macro.replace(macro.three, compatible_replacement) - macro(one__x=0) + downstream.pull() self.assertEqual( len(downstream.inputs.x.connections), 1, @@ -457,7 +455,10 @@ def different_signature(x): 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 + macro > downstream + # If we want to push, we need to define a connection formally + macro.run(one__x=1) + # Fresh input to make sure updates are actually going through self.assertEqual( 1 + (1 + 1 + 2) + 1, downstream.outputs.result.value, @@ -484,7 +485,8 @@ def different_signature(x): 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 + macro > downstream # Manually specify since we'll run the macro but look + # at the downstream output, and none of this is happening in a workflow original_one = macro.one macro.executor = True @@ -495,7 +497,7 @@ def test_with_executor(self): msg="Sanity check that test is in right starting condition" ) - result = macro(one__x=0) + result = macro.run(one__x=0) self.assertIsInstance( result, Future, From e66aa2b545d1933f32b6d3380db13be7e8b42517 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 1 Nov 2023 09:09:01 -0700 Subject: [PATCH 14/35] :bug: complete variable rename --- 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 80616f1e..d6bbdbce 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -331,7 +331,7 @@ def run_data_tree(self, run_parent_trees_too=False) -> None: is False, only run nodes in this scope, i.e. sharing the same parent.) """ if run_parent_trees_too and self.parent is not None: - self.parent.run_data_tree(pull_from_parents=True) + self.parent.run_data_tree(run_parent_trees_too=True) label_map = {} nodes = {} From 33de38fe324687d2922cfc7f970a0b14b20d0a69 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 1 Nov 2023 09:14:14 -0700 Subject: [PATCH 15/35] :bug: update parent input after running its data tree --- pyiron_workflow/node.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index d6bbdbce..5ca41aff 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -332,6 +332,7 @@ def run_data_tree(self, run_parent_trees_too=False) -> None: """ if run_parent_trees_too and self.parent is not None: self.parent.run_data_tree(run_parent_trees_too=True) + self.parent.inputs.fetch() label_map = {} nodes = {} From 91f692fce0084b87114886f1c3a05a2a0bfe3ee5 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 1 Nov 2023 09:14:30 -0700 Subject: [PATCH 16/35] Test pulling inside and outside scope --- tests/unit/test_macro.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 3f28a4ff..df1b31f6 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -554,6 +554,29 @@ def test_with_executor(self): "downstream execution" ) + def test_pulling_from_inside_a_macro(self): + upstream = SingleValue(add_one, x=2) + macro = Macro(add_three_macro, one__x=upstream) + macro.inputs.one__x = 0 # Set value + # Now macro.one.inputs.x has both value and a connection + + print("MACRO ONE INPUT X", macro.one.inputs.x.value, macro.one.inputs.x.connections) + + self.assertEqual( + 0 + 1 + 1, + macro.two.pull(run_parent_trees_too=False), + msg="Without running parent trees, the pulling should only run upstream " + "nodes _inside_ the scope of the macro, relying on the explicit input" + "value" + ) + + self.assertEqual( + (2 + 1) + 1 + 1, + macro.two.pull(run_parent_trees_too=True), + msg="Running with parent trees, the pulling should also run the parents " + "data dependencies first" + ) + if __name__ == '__main__': unittest.main() From cb3940619a6e0bafe83596483b8eadc0a379269c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 1 Nov 2023 13:33:47 -0700 Subject: [PATCH 17/35] Fix docs to hint the type of error that is actually raised And raise from context --- pyiron_workflow/topology.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyiron_workflow/topology.py b/pyiron_workflow/topology.py index 06d53631..c7084212 100644 --- a/pyiron_workflow/topology.py +++ b/pyiron_workflow/topology.py @@ -95,7 +95,7 @@ def nodes_to_execution_order(nodes: dict[str, Node]) -> list[str]: (list[str]): The labels in safe execution order. Raises: - CircularDependencyError: If the data dependency is not a Directed Acyclic Graph + ValueError: If the data dependency is not a Directed Acyclic Graph """ try: # Topological sorting ensures that all input dependencies have been @@ -107,7 +107,7 @@ def nodes_to_execution_order(nodes: dict[str, Node]) -> list[str]: 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}" - ) + ) from e return execution_order From 3bec8a9c7fa550ec0efa637816e956385b106b59 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 1 Nov 2023 13:46:44 -0700 Subject: [PATCH 18/35] Extract the data tree node search to the topology module --- pyiron_workflow/node.py | 16 ++++------------ pyiron_workflow/topology.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 5ca41aff..e45d4077 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -16,7 +16,9 @@ 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.topology import set_run_connections_according_to_linear_dag +from pyiron_workflow.topology import ( + get_nodes_in_data_tree, set_run_connections_according_to_linear_dag +) from pyiron_workflow.util import SeabornColors if TYPE_CHECKING: @@ -336,7 +338,7 @@ def run_data_tree(self, run_parent_trees_too=False) -> None: label_map = {} nodes = {} - for node in self.get_nodes_in_data_tree(): + for node in get_nodes_in_data_tree(self): modified_label = node.label + str(id(node)) label_map[modified_label] = node.label node.label = modified_label # Ensure each node has a unique label @@ -373,16 +375,6 @@ def run_data_tree(self, run_parent_trees_too=False) -> None: 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 - @manage_status def _run( self, diff --git a/pyiron_workflow/topology.py b/pyiron_workflow/topology.py index c7084212..a8f725ce 100644 --- a/pyiron_workflow/topology.py +++ b/pyiron_workflow/topology.py @@ -154,3 +154,20 @@ def set_run_connections_according_to_linear_dag( for c1, c2 in disconnected_pairs: c1.connect(c2) raise e + + +def get_nodes_in_data_tree(node: Node) -> set[Node]: + """ + Get a set of all nodes from this one and upstream through data connections. + """ + try: + nodes = set([node]) + for channel in node.inputs: + for connection in channel.connections: + nodes = nodes.union(get_nodes_in_data_tree(connection.node)) + return nodes + except RecursionError: + raise ValueError( + f"Detected a cycle in the data flow topology for {node.label}, unable to " + f"extract nodes from here upstream because upstream is not well defined." + ) From 479ea58cec8875a645e3dcecb9fba1bc1eae702d Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 1 Nov 2023 13:52:57 -0700 Subject: [PATCH 19/35] Introduce a special error class for readability --- pyiron_workflow/topology.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pyiron_workflow/topology.py b/pyiron_workflow/topology.py index a8f725ce..baaf8c09 100644 --- a/pyiron_workflow/topology.py +++ b/pyiron_workflow/topology.py @@ -15,6 +15,10 @@ from pyiron_workflow.node import Node +class CircularDataFlowError(ValueError): + pass + + 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 @@ -29,7 +33,7 @@ def nodes_to_data_digraph(nodes: dict[str, Node]) -> dict[str, set[str]]: data. Raises: - ValueError: When a node appears in its own input. + CircularDataFlowError: 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. @@ -72,7 +76,7 @@ def nodes_to_data_digraph(nodes: dict[str, Node]) -> dict[str, set[str]]: # 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( + raise CircularDataFlowError( 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." ) @@ -104,7 +108,7 @@ def nodes_to_execution_order(nodes: dict[str, Node]) -> list[str]: # 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( + raise CircularDataFlowError( f"Detected a cycle in the data flow topology, unable to automate the " f"execution of non-DAGs: cycles found among {e.data}" ) from e @@ -167,7 +171,8 @@ def get_nodes_in_data_tree(node: Node) -> set[Node]: nodes = nodes.union(get_nodes_in_data_tree(connection.node)) return nodes except RecursionError: - raise ValueError( - f"Detected a cycle in the data flow topology for {node.label}, unable to " - f"extract nodes from here upstream because upstream is not well defined." + raise CircularDataFlowError( + f"Detected a cycle in the data flow topology, unable to automate the " + f"execution of non-DAGs: finding the upstream nodes for {node.label} hit a " + f"recursion error." ) From fc5b3de8663565c6ce51c2d10436f60e52aea7a6 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 1 Nov 2023 13:53:12 -0700 Subject: [PATCH 20/35] Test failures when the pulled node has cyclic data flow --- tests/unit/test_macro.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index df1b31f6..46df3bbe 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -7,6 +7,7 @@ from pyiron_workflow.channels import NotData from pyiron_workflow.function import SingleValue from pyiron_workflow.macro import Macro +from pyiron_workflow.topology import CircularDataFlowError def add_one(x): @@ -577,6 +578,44 @@ def test_pulling_from_inside_a_macro(self): "data dependencies first" ) + def test_recovery_after_failed_pull(self): + + def cyclic_macro(macro): + macro.one = SingleValue(add_one) + macro.two = SingleValue(add_one, x=macro.one) + macro.one.inputs.x = macro.two + macro.one > macro.two + macro.starting_nodes = [macro.one] + # We need to manually specify execution since the data flow is cyclic + + m = Macro(cyclic_macro) + + initial_labels = list(m.nodes.keys()) + + def grab_connections(macro): + return macro.one.inputs.x.connections +\ + macro.two.inputs.x.connections +\ + macro.one.signals.input.connections +\ + macro.two.signals.input.connections + + initial_connections = grab_connections(m) + + with self.assertRaises( + CircularDataFlowError, + msg="Pull should only work for DAG workflows" + ): + m.two.pull() + self.assertListEqual( + initial_labels, + list(m.nodes.keys()), + msg="Labels should be restored after failing to pull because of acyclicity" + ) + self.assertTrue( + all(c is ic for (c, ic) in zip(grab_connections(m), initial_connections)), + msg="Connections should be restored after failing to pull because of " + "acyclicity" + ) + if __name__ == '__main__': unittest.main() From 5acad7b85180dc27cd8e8e4fbee68e7e9d1afcef Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 1 Nov 2023 14:20:46 -0700 Subject: [PATCH 21/35] Test recovery after other failures Should have been separate commits for cyclic data in parent context and when an upstream node fails, but we'll live. --- tests/unit/test_macro.py | 128 ++++++++++++++++++++++++++++++--------- 1 file changed, 98 insertions(+), 30 deletions(-) diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 46df3bbe..0ee9d78b 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -579,42 +579,110 @@ def test_pulling_from_inside_a_macro(self): ) def test_recovery_after_failed_pull(self): + def grab_x_and_run(node): + """Grab a couple connections from an add_one-like node""" + return node.inputs.x.connections + node.signals.input.run.connections - def cyclic_macro(macro): - macro.one = SingleValue(add_one) - macro.two = SingleValue(add_one, x=macro.one) - macro.one.inputs.x = macro.two - macro.one > macro.two - macro.starting_nodes = [macro.one] - # We need to manually specify execution since the data flow is cyclic + with self.subTest("When the local scope has cyclic data flow"): + def cyclic_macro(macro): + macro.one = SingleValue(add_one) + macro.two = SingleValue(add_one, x=macro.one) + macro.one.inputs.x = macro.two + macro.one > macro.two + macro.starting_nodes = [macro.one] + # We need to manually specify execution since the data flow is cyclic - m = Macro(cyclic_macro) + m = Macro(cyclic_macro) - initial_labels = list(m.nodes.keys()) + initial_labels = list(m.nodes.keys()) - def grab_connections(macro): - return macro.one.inputs.x.connections +\ - macro.two.inputs.x.connections +\ - macro.one.signals.input.connections +\ - macro.two.signals.input.connections + def grab_connections(macro): + return grab_x_and_run(macro.one) + grab_x_and_run(macro.two) - initial_connections = grab_connections(m) + initial_connections = grab_connections(m) - with self.assertRaises( - CircularDataFlowError, - msg="Pull should only work for DAG workflows" - ): - m.two.pull() - self.assertListEqual( - initial_labels, - list(m.nodes.keys()), - msg="Labels should be restored after failing to pull because of acyclicity" - ) - self.assertTrue( - all(c is ic for (c, ic) in zip(grab_connections(m), initial_connections)), - msg="Connections should be restored after failing to pull because of " - "acyclicity" - ) + with self.assertRaises( + CircularDataFlowError, + msg="Pull should only work for DAG workflows" + ): + m.two.pull() + self.assertListEqual( + initial_labels, + list(m.nodes.keys()), + msg="Labels should be restored after failing to pull because of acyclicity" + ) + self.assertTrue( + all(c is ic for (c, ic) in zip(grab_connections(m), initial_connections)), + msg="Connections should be restored after failing to pull because of " + "cyclic data flow" + ) + + with self.subTest("When the parent scope has cyclic data flow"): + n1 = SingleValue(add_one, label="n1", x=0) + n2 = SingleValue(add_one, label="n2", x=n1) + m = Macro(add_three_macro, label="m", one__x=n2) + + self.assertEqual( + 0 + 1 + 1 + (1 + 1 + 1), + m.three.pull(run_parent_trees_too=True), + msg="Sanity check, without cyclic data flows pulling here should be ok" + ) + + n1.inputs.x = n2 + + initial_connections = grab_x_and_run(n1) + grab_x_and_run(n2) + with self.assertRaises( + CircularDataFlowError, + msg="Once the outer scope has circular data flows, pulling should fail" + ): + m.three.pull(run_parent_trees_too=True) + self.assertTrue( + all( + c is ic + for (c, ic) in zip( + grab_x_and_run(n1) + grab_x_and_run(n2), initial_connections + ) + ), + msg="Connections should be restored after failing to pull because of " + "cyclic data flow in the outer scope" + ) + self.assertEqual( + "n1", + n1.label, + msg="Labels should get restored in the outer scope" + ) + self.assertEqual( + "one", + m.one.label, + msg="Labels should not have even gotten perturbed to start with in the" + "inner scope" + ) + + with self.subTest("When a node breaks upstream"): + def fail_at_zero(x): + y = 1 / x + return y + + n1 = SingleValue(fail_at_zero, x=0) + n2 = SingleValue(add_one, x=n1, label="n1") + n_not_used = SingleValue(add_one) + n_not_used > n2 # Just here to make sure it gets restored + + with self.assertRaises( + ZeroDivisionError, + msg="The underlying error should get raised" + ): + n2.pull() + self.assertEqual( + "n1", + n2.label, + msg="Original labels should get restored on upstream failure" + ) + self.assertIs( + n_not_used, + n2.signals.input.run.connections[0].node, + msg="Original connections should get restored on upstream failure" + ) if __name__ == '__main__': From 653c79583b3f9b4f2a6891866b35fc196ac89358 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 1 Nov 2023 14:25:00 -0700 Subject: [PATCH 22/35] Reproduce integration test as a unit test For pulling when all the nodes are parentless --- tests/unit/test_function.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 28b8fbb8..1a33fcc1 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -696,6 +696,19 @@ def test_disconnection(self): "on) of all broken connections among input, output, and signals." ) + def test_pulling_without_any_parents(self): + node = SingleValue( + plus_one, + x=SingleValue( + plus_one, + x=SingleValue( + plus_one, + x=2 + ) + ) + ) + self.assertEqual(2 + 1 + 1 + 1, node.pull()) + if __name__ == '__main__': unittest.main() From 1178efbd6c5d5aaa8d0955c84b2ffe903ffafa73 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 1 Nov 2023 14:25:34 -0700 Subject: [PATCH 23/35] Remove pull integration tests They are duplicated in integration tests. In particular, we check pulling from inside a macro in the macro tests --- tests/integration/test_pull.py | 84 ---------------------------------- 1 file changed, 84 deletions(-) delete mode 100644 tests/integration/test_pull.py diff --git a/tests/integration/test_pull.py b/tests/integration/test_pull.py deleted file mode 100644 index 75e85aca..00000000 --- a/tests/integration/test_pull.py +++ /dev/null @@ -1,84 +0,0 @@ -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 From 1777aeccd4448db93f197ab59484420ac7b44a18 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 1 Nov 2023 14:44:27 -0700 Subject: [PATCH 24/35] Just let workflows silently not run data trees Since it can't have one and its parent is always none. This prevents an error when a workflow sits at the top of a recursive data running call. --- pyiron_workflow/workflow.py | 6 ------ tests/unit/test_workflow.py | 8 -------- 2 files changed, 14 deletions(-) diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 176fb653..b9844128 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -228,12 +228,6 @@ def pull(self, run_parent_trees_too=False, **kwargs): """Workflows are a parent-most object, so this simply runs without pulling.""" return self.run(**kwargs) - def run_data_tree(self, run_parent_trees_too=False) -> None: - raise NotImplementedError( - f"{self.__class__.__name__} must be a parent-most node, and therefore has " - f"no upstream data tree to run." - ) - def to_node(self): """ Export the workflow to a macro node, with the currently exposed IO mapped to diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index 345531c9..fc7b8775 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -462,14 +462,6 @@ def test_io_label_maps_are_bijective(self): with self.assertRaises(ValueDuplicationError): wf.inputs_map["foo2__x"] = "x1" - def test_run_data_tree(self): - wf = Workflow("parent_most") - with self.assertRaises( - NotImplementedError, - msg="Workflows are a parent-most object" - ): - wf.run_data_tree() - if __name__ == '__main__': unittest.main() From c40c92a4210c6fc77b3ab555bb766cc8a99a19ca Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 1 Nov 2023 15:05:16 -0700 Subject: [PATCH 25/35] Strictly disallow mixing pull mode and executors The presence of the executors indicates to me that you might be sitting around waiting for the result longer than expected; IMO it's healthier to push users towards actually putting things in a workflow and then invoking the workflow when they get to the point that they want to be using executors. --- pyiron_workflow/node.py | 12 +++++++++++- tests/unit/test_workflow.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index e45d4077..f5bfef79 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -338,7 +338,17 @@ def run_data_tree(self, run_parent_trees_too=False) -> None: label_map = {} nodes = {} - for node in get_nodes_in_data_tree(self): + + data_tree_nodes = get_nodes_in_data_tree(self) + for node in data_tree_nodes: + if node.executor: + raise ValueError( + f"Running the data tree is pull-paradigm action, and is " + f"incompatible with using executors. An executor request was found " + f"on {node.label}" + ) + + for node in data_tree_nodes: modified_label = node.label + str(id(node)) label_map[modified_label] = node.label node.label = modified_label # Ensure each node has a unique label diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index fc7b8775..537bb8ac 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -462,6 +462,42 @@ def test_io_label_maps_are_bijective(self): with self.assertRaises(ValueDuplicationError): wf.inputs_map["foo2__x"] = "x1" + def test_pull_and_executors(self): + def add_three_macro(macro): + macro.one = Workflow.create.SingleValue(plus_one) + macro.two = Workflow.create.SingleValue(plus_one, x=macro.one) + macro.three = Workflow.create.SingleValue(plus_one, x=macro.two) + + wf = Workflow("pulling") + + wf.n1 = Workflow.create.SingleValue(plus_one, x=0) + wf.m = Workflow.create.Macro(add_three_macro, one__x=wf.n1) + + self.assertEquals( + (0 + 1) + (1 + 1), + wf.m.two.pull(run_parent_trees_too=True), + msg="Sanity check, pulling here should work perfectly fine" + ) + + wf.m.one.executor = True + with self.assertRaises( + ValueError, + msg="Should not be able to pull with executor in local scope" + ): + wf.m.two.pull() + wf.m.one.executor = False + + wf.n1.executor = True + with self.assertRaises( + ValueError, + msg="Should not be able to pull with executor in parent scope" + ): + wf.m.two.pull(run_parent_trees_too=True) + + # Pulling in the local scope should be fine with an executor only in the parent + # scope + wf.m.two.pull(run_parent_trees_too=False) + if __name__ == '__main__': unittest.main() From a21634e7cf6594815ebd7bc0a2b949725d910022 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Wed, 1 Nov 2023 22:07:54 +0000 Subject: [PATCH 26/35] Format black --- pyiron_workflow/node.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index f5bfef79..8ecf94aa 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -17,7 +17,8 @@ from pyiron_workflow.has_to_dict import HasToDict from pyiron_workflow.io import Signals, InputSignal, OutputSignal from pyiron_workflow.topology import ( - get_nodes_in_data_tree, set_run_connections_according_to_linear_dag + get_nodes_in_data_tree, + set_run_connections_according_to_linear_dag, ) from pyiron_workflow.util import SeabornColors @@ -338,7 +339,7 @@ def run_data_tree(self, run_parent_trees_too=False) -> None: label_map = {} nodes = {} - + data_tree_nodes = get_nodes_in_data_tree(self) for node in data_tree_nodes: if node.executor: From 9081bc5c2b9a904636ef39460dbb93d98c73e6bd Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Nov 2023 11:09:33 -0700 Subject: [PATCH 27/35] Update Node docs --- pyiron_workflow/node.py | 75 ++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 8ecf94aa..ee90eee3 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -72,9 +72,14 @@ class Node(HasToDict, ABC): Together these channels represent edges on the dual data and execution computational graphs. - Nodes can be run to force their computation, or more gently updated, which will - trigger a run only if all of the input is ready (i.e. channel values conform to - any type hints provided). + Nodes can be run in a variety of ways.. + Non-exhaustively, they can be run in a "push" paradigm where they do their + calculation and then trigger downstream calculations; in a "pull" mode where they + first make sure all their upstream dependencies then run themselves (but not + anything downstream); or they may be forced to run their calculation with exactly + the input they have right now. + These and more options are available, and for more information look at the `run` + method. Nodes may have a `parent` node that owns them as part of a sub-graph. @@ -95,15 +100,15 @@ class Node(HasToDict, ABC): channels. 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`. Both `run()` and `update()` will raise - errors if the node is already running or has a failed status. + object, if the node is running on an executor), and consequently the `pull`, + `execute`, and `__call__` shortcuts to `run` also return the same thing. - 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 - all supplied input and then calling `run()`. - As such, calling the node _also_ returns a representation of the output (or `None` - if the node is not set to run on updates, or is otherwise unready to run). + Invoking the `run` method (or one of its aliases) of an already instantiated node + allows its input channels to be updated using keyword arguments corresponding to + the channel labels, performing a batch-update of all supplied input and then + proceeding. + As such, _if_ the run invocation updates the input values some other way, these + supplied values will get overwritten. Nodes have a status, which is currently represented by the `running` and `failed` boolean flag attributes. @@ -119,15 +124,16 @@ class Node(HasToDict, ABC): with the resulting future object. WARNING: Executors are currently only working when the node executable function does not use `self`. + NOTE: Executors are only allowed in a "push" paradigm, and you will get an + exception if you try to `pull` and one of the upstream nodes uses an executor. This is an abstract class. - Children *must* define how `inputs` and `outputs` are constructed, and what will - happen `on_run`. - They may also override the `run_args` property to specify input passed to the - defined `on_run` method, and may add additional signal channels to the signals IO. + Children *must* define how `inputs` and `outputs` are constructed, what will + happen `on_run`, the `run_args` that will get passed to `on_run`, and how to + `process_run_result` once `on_run` finishes. + They may optionally add additional signal channels to the signals IO. - # TODO: Everything with (de)serialization and executors for running on something - # other than the main python process. + # TODO: Everything with (de)serialization for storage Attributes: connected (bool): Whether _any_ of the IO (including signals) are connected. @@ -160,20 +166,24 @@ 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). + __call__: An alias for `pull` that aggressively runs upstream nodes even + _outside_ the local scope (i.e. runs parents' dependencies as well). disconnect: Remove all connections, including signals. draw: Use graphviz to visualize the node, its IO and, if composite in nature, its internal structure. - execute: Run the node, but right here, right now, and with the input it - currently has. + execute: An alias for `run`, but with flags to run 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. + pull: An alias for `run` that runs everything upstream, then runs this node + (but doesn't fire off the `ran` signal, so nothing happens farther + downstream). "Upstream" may optionally break out of the local scope to run + parent nodes' dependencies as well (all the way until the parent-most + object is encountered). + run: Run the node function from `on_run`. Handles status automatically. Various + execution options are available as boolean flags. + set_input_values: Allows input channels' values to be updated without any + running. """ def __init__( @@ -266,6 +276,12 @@ def run( If executor information is specified, execution happens on that process, a callback is registered, and futures object is returned. + Input values can be updated at call time with kwargs, but this happens _first_ + so any input updates that happen as a result of the computation graph will + override these by default. If you really want to execute the node with a + particular set of input, set it all manually and use `execute` (or `run` with + carefully chosen flags). + Args: run_data_tree (bool): Whether to first run all upstream nodes in the data graph. (Default is False.) @@ -292,9 +308,7 @@ def run( Note: Kwargs updating input channel values happens _first_ and will get - overwritten by any subsequent graph-based data manipulation. If you really - want to execute the node with a particular set of input, set it all manually - and use `execute` (or `run` with carefully chosen flags). + overwritten by any subsequent graph-based data manipulation. """ self.set_input_values(**kwargs) @@ -484,7 +498,8 @@ def pull(self, run_parent_trees_too=False, **kwargs): def __call__(self, **kwargs) -> None: """ - A shortcut for `run`. + A shortcut for `pull` that automatically runs the entire set of upstream data + dependencies all the way to the parent-most graph object. """ return self.pull(run_parent_tree_too=True, **kwargs) From d3362ae50ee9cbb6a21bcf555d6062ab1ace42e0 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Nov 2023 11:35:17 -0700 Subject: [PATCH 28/35] Update Function docs --- pyiron_workflow/function.py | 92 ++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 38 deletions(-) diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index e9a6ce12..aeda96ae 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -47,7 +47,7 @@ class Function(Node): Actual function node instances can either be instances of the base node class, in which case the callable node function *must* be provided OR they can be instances - of children of this class. + of children of this class which provide the node function as a class-level method. Those children may define some or all of the node behaviour at the class level, and modify their signature accordingly so this is not available for alteration by the user, e.g. the node function and output labels may be hard-wired. @@ -65,10 +65,8 @@ class Function(Node): After a node is instantiated, its input can be updated as `*args` and/or `**kwargs` on call. - `run()` returns the output of the executed function, or a futures object if the - node is set to use an executor. - Calling the node or executing an `update()` returns the same thing as running, if - the node is run, or, in the case of `update()`, `None` if it is not `ready` to run. + `run()` and its aliases return the output of the executed function, or a futures + object if the node is set to use an executor. Args: node_function (callable): The function determining the behaviour of the node. @@ -88,23 +86,6 @@ class Function(Node): **kwargs: Any additional keyword arguments whose keyword matches the label of an input channel will have their value assigned to that channel. - Attributes: - inputs (Inputs): A collection of input data channels. - outputs (Outputs): A collection of output data channels. - signals (Signals): A holder for input and output collections of signal channels. - ready (bool): All input reports ready, node is not running or failed. - running (bool): Currently running. - failed (bool): An exception was thrown when executing the node function. - connected (bool): Any IO channel has at least one connection. - fully_connected (bool): Every IO channel has at least one connection. - - Methods: - update: If your input is ready, will run the engine. - run: Parse and process the input, execute the engine, process the results and - update the output. - disconnect: Disconnect all data and signal IO connections. - 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 `Function` class with a function and labels for its output, like so: @@ -127,10 +108,15 @@ class Function(Node): run: >>> plus_minus_1.inputs.x = 2 >>> plus_minus_1.run() - TypeError: unsupported operand type(s) for -: 'type' and 'int' + ValueError: mwe 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: True + y ready: False - This is because the second input (`y`) still has no input value, so we can't do - the sum between `NotData` and `2`. + This is because the second input (`y`) still has no input value -- indicated in + the error message -- so we can't do the sum between `NotData` and `2`. Once we update `y`, all the input is ready we will be allowed to proceed to a `run()` call, which succeeds and updates the output. @@ -152,9 +138,8 @@ class Function(Node): Input data can be provided to both initialization and on call as ordered args or keyword kwargs. - When running, updating, or calling the node, the output of the wrapped function - (if it winds up getting run in the conditional cases of updating and calling) is - returned: + When running the node (or any alias to run like pull, execute, or just calling + the node), the output of the wrapped function is returned: >>> plus_minus_1(2, y=3) (3, 2) @@ -172,7 +157,7 @@ class Function(Node): Note that getting "good" (i.e. dot-accessible) output labels can be achieved by 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 + If we force the node to run with bad types, it will raise an error: >>> from typing import Union >>> @@ -259,24 +244,55 @@ class Function(Node): 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. - Then we need to define the corresponding execution flow, which can be done - by directly connecting `.signals.input.run` and `.signals.output.ran` channels - just like we connect data channels, but can also be accomplished with some - syntactic sugar using the `>` operator. - When we update the upstream node, we'll see the result passed downstream: - >>> adder = Adder() + At the end of the day, the graph will also need to know about the execution + flow, but in most cases (directed acyclic graphs -- DAGs), this can be worked + out automatically by the topology of data connections. + Let's put together a couple of nodes and then run in a "pull" paradigm to get + the final node to run everything "upstream" then run itself: + >>> @function_node() + ... def adder_node(x: int = 0, y: int = 0) -> int: + ... sum = x + y + ... return sum + >>> + >>> adder = adder_node(x=1) + >>> alpha = AlphabetModThree(i=adder.outputs.sum) + >>> print(alpha()) + "b" + >>> adder.inputs.y = 1 + >>> print(alpha()) + "c" + >>> adder.inputs.x = 0 + >>> adder.inputs.y = 0 + >>> print(alpha()) + "a" + + Alternatively, execution flows can be specified manualy by connecting + `.signals.input.run` and `.signals.output.ran` channels, either by their + `.connect` method or by assignment (both cases just like data chanels), or by + some syntactic sugar using the `>` operator. + Then we can use a "push" paradigm with the `run` command to force execution + forwards through the graph to get an end result. + This is a bit more verbose, but a necessary tool for more complex situations + (like cyclic graphs). + Here's our simple example from above using this other paradigm: + >>> @function_node() + ... def adder_node(x: int = 0, y: int = 0) -> int: + ... sum = x + y + ... return sum + >>> + >>> adder = adder_node() >>> alpha = AlphabetModThree(i=adder.outputs.sum) >>> adder > alpha >>> - >>> adder(x=1) + >>> adder.run(x=1) >>> print(alpha.outputs.letter) "b" - >>> adder(y=1) + >>> adder.run(y=1) >>> print(alpha.outputs.letter) "c" >>> adder.inputs.x = 0 >>> adder.inputs.y = 0 - >>> adder() + >>> adder.run() >>> print(alpha.outputs.letter) "a" From 258eaa83bd34e028de3a26a7f2df9c27ff1a0dd0 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Nov 2023 11:36:49 -0700 Subject: [PATCH 29/35] Update SingleValue docs --- pyiron_workflow/function.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index aeda96ae..dfb633a0 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -585,6 +585,9 @@ class SingleValue(Function, HasChannel): Note that this means any attributes/method available on the output value become available directly at the node level (at least those which don't conflict with the existing node namespace). + + This also allows the entire node to be used as a reference to its output channel + when making data connections, e.g. `some_node.input.some_channel = my_svn_instance`. """ def __init__( From 12133f221a5cd5763d389848d5dc64453f2a4737 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Nov 2023 11:40:53 -0700 Subject: [PATCH 30/35] Update Composite docs --- pyiron_workflow/composite.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 3b1fce86..3dfab32e 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -41,16 +41,17 @@ class Composite(Node, ABC): instances, any created nodes get their `parent` attribute automatically set to the composite instance being used. - Specifies the required `on_run()` to call `run()` on a subset of owned - `starting_nodes`nodes to kick-start computation on the owned sub-graph. + Specifies the required `on_run()` and `run_args` to call `run()` on a subset of + owned `starting_nodes`, thus kick-starting computation on the owned sub-graph. Both the specification of these starting nodes and specifying execution signals to propagate execution through the graph is left to the user/child classes. In the case of non-cyclic workflows (i.e. DAGs in terms of data flow), both - starting nodes and execution flow can be specified by invoking `` + starting nodes and execution flow can be specified by invoking execution flow can + be determined automatically. - The `run()` method (and `update()`, and calling the workflow) return a new - dot-accessible dictionary of keys and values created from the composite output IO - panel. + Also specifies `process_run_result` such that the `run` method (and its aliases) + return a new dot-accessible dictionary of keys and values created from the + composite output IO panel. Does not specify `input` and `output` as demanded by the parent class; this requirement is still passed on to children. @@ -81,10 +82,6 @@ class Composite(Node, ABC): add(node: Node): Add the node instance to this subgraph. remove(node: Node): Break all connections the node has, remove it from this subgraph, and set its parent to `None`. - - TODO: - Wrap node registration at the class level so we don't need to do - `X.create.register` but can just do `X.register` """ wrap_as = Wrappers() From bc546a788a56c63086434d360ee17f9f445e2683 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Nov 2023 11:45:20 -0700 Subject: [PATCH 31/35] Update Macro docs --- pyiron_workflow/macro.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index e81d4975..b68eaaf2 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -35,7 +35,7 @@ class Macro(Composite): It is intended that subclasses override the initialization signature and provide the graph creation directly from their own method. - As with workflows, all DAG macros will determine their execution flow automatically, + As with workflows, all DAG macros can determine their execution flow automatically, if you have cycles in your data flow, or otherwise want more control over the execution, all you need to do is specify the `node.signals.input.run` connections and `starting_nodes` list yourself. @@ -155,7 +155,7 @@ class Macro(Composite): >>> 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) + >>> adds_six_macro(one__x=1) {'three__result': 7} """ From 26461b3a7e5ca69d57a38cc12bd3032de1127cd7 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Nov 2023 11:51:55 -0700 Subject: [PATCH 32/35] Update Workflow docs --- pyiron_workflow/workflow.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index b9844128..2b7611a6 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -29,8 +29,8 @@ class Workflow(Composite): They are then accessible either under the `nodes` dot-dictionary, or just directly by dot-access on the workflow object itself. - Using the `input` and `output` attributes, the workflow gives access to all the - IO channels among its nodes which are currently unconnected. + Using the `input` and `output` attributes, the workflow gives by-reference access + to all the IO channels among its nodes which are currently unconnected. The `Workflow` class acts as a single-point-of-import for us; Directly from the class we can use the `create` method to instantiate workflow @@ -38,6 +38,14 @@ class Workflow(Composite): When called from a workflow _instance_, any created nodes get their parent set to the workflow instance being used. + Workflows are "living" -- i.e. their IO is always by reference to their owned nodes + and you are meant to add and remove nodes as children -- and "parent-most" -- i.e. + they sit at the top of any data dependency tree and may never have a parent of + their own. + They are flexible and great for development, but once you have a setup you like, + you should consider reformulating it as a `Macro`, which operates somewhat more + efficiently. + Examples: We allow adding nodes to workflows in five equivalent ways: >>> from pyiron_workflow.workflow import Workflow @@ -116,8 +124,10 @@ class Workflow(Composite): 12 Workflows also give access to packages of pre-built nodes under different - namespaces, e.g. + namespaces. These need to be registered first. >>> wf = Workflow("with_prebuilt") + >>> wf.register("atomistics", "pyiron_workflow.node_library.atomistics") + >>> wf.register("plotting", "pyiron_workflow.node_library.plotting") >>> >>> wf.structure = wf.create.atomistics.Bulk( ... cubic=True, @@ -127,7 +137,7 @@ class Workflow(Composite): >>> wf.calc = wf.create.atomistics.CalcMd( ... job=wf.engine, ... ) - >>> wf.plot = wf.create.standard.Scatter( + >>> wf.plot = wf.create.plotting.Scatter( ... x=wf.calc.outputs.steps, ... y=wf.calc.outputs.temperature ... ) From fcafd6e8dc133bd55a579e35c0b7ec5ba85dc773 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Nov 2023 13:25:01 -0700 Subject: [PATCH 33/35] :bug: Fix typo --- 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 ee90eee3..14f33aae 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -501,7 +501,7 @@ def __call__(self, **kwargs) -> None: A shortcut for `pull` that automatically runs the entire set of upstream data dependencies all the way to the parent-most graph object. """ - return self.pull(run_parent_tree_too=True, **kwargs) + return self.pull(run_parent_trees_too=True, **kwargs) def set_input_values(self, **kwargs) -> None: """ From 1b9e7c819c370c80d0791c41521bfe3415f229e5 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Nov 2023 14:03:03 -0700 Subject: [PATCH 34/35] Update notebook --- notebooks/workflow_example.ipynb | 742 +++++++++++++++++-------------- 1 file changed, 409 insertions(+), 333 deletions(-) diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index defccc32..def5e0a6 100644 --- a/notebooks/workflow_example.ipynb +++ b/notebooks/workflow_example.ipynb @@ -312,7 +312,7 @@ } ], "source": [ - "adder_node(10, y=20)\n", + "adder_node = Function(adder, 10, y=20)\n", "adder_node.run()" ] }, @@ -447,15 +447,17 @@ "source": [ "# Connecting nodes and controlling flow\n", "\n", - "Multiple nodes can be used together to build a computational graph, with each node performing a particular operation in the overall workflow:\n", + "Multiple nodes can be used together to build a computational graph, with each node performing a particular operation in the overall workflow.\n", "\n", - "The input and output of nodes can be chained together by connecting their data channels. When a node runs, its output channels will push their new value to each input node to whom they are connected. In this way, data propagates forwards\n", + "The input and output of nodes can be chained together by connecting their data channels.\n", "\n", - "In addition to input and output data channels, nodes also have \"signal\" channels available. Input signals are bound to a callback function (typically one of its node's methods), and output signals trigger the callbacks for all the input signal channels they're connected to.\n", + "The flow of execution can be manually configured by using other \"signal\" channels. However, for acyclic graphs (DAGs), execution flow can be automatically determined from the topology of the data connections.\n", "\n", - "Standard nodes have a `run` input signal (which is, unsurprisingly, bound to the `run` method), and a `ran` output signal (which, again, hopefully with no great surprise, is triggered at the end of the `run` method.)\n", + "The `run` command we saw above has several boolean flags for controlling the style of execution. The two main run modes are with a \"pull\" paradigm, where everything upstream is run first then the node invoking `pull` gets run; and with a \"push\" paradigm (the default for `run`), where the node invoking `run` gets run and then runs everything downstream. Calling an instantiated node runs a particularly aggressive version of `pull`.\n", "\n", - "In the example below we see how this works for a super-simple toy graph:" + "We'll talk more about grouping nodes together inside a `Workflow` object, but without a parent workflow, only the `pull` method will automate execution signals; trying to push data downstream using `run` requires specifying the execution flow manually.\n", + "\n", + "Let's start by looking at `pull` in the example below to see how this works for a super-simple toy graph:" ] }, { @@ -467,11 +469,24 @@ }, "outputs": [ { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "1 2\n" + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel ran was not connected to run, 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" ] + }, + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ @@ -490,8 +505,37 @@ "t2.inputs.x = l.outputs.x\n", "t2.signals.input.run = l.signals.output.ran\n", "\n", - "l.run()\n", - "print(t2.inputs.x, t2.outputs.double)" + "t2.pull()" + ] + }, + { + "cell_type": "markdown", + "id": "09623591-bbbb-462c-b490-f1db02c9f459", + "metadata": {}, + "source": [ + "And, as mentioned, `__call__` is just (roughly) an alias for `pull`:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "f3b0b700-683e-43cb-b374-48735e413bc9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "l.inputs.x = 2\n", + "t2()" ] }, { @@ -499,18 +543,18 @@ "id": "5da1ecfc-7145-4fb2-b5c0-417f050c5de4", "metadata": {}, "source": [ - "We can use a couple pieces of syntactic sugar to make this faster.\n", + "Next, lets see how to do this with a \"push\" paradigm.\n", "\n", - "First: data connections can be made with keyword arguments just like other input data definitions.\n", + "Just like the data connections, we can connect the `.signals.inputs.run` and `.signals.output.ran` channels of two nodes, but we can also use the `>` operator as a syntactic sugar shortcut.\n", "\n", - "Second: the `>` is a shortcut for creating connections between the left-hand node's `signals.output.ran` channel and the right-hand node's `signals.input.run` channel.\n", + "Note how data connections can be made with keyword arguments just like other input data definitions.\n", "\n", "With both of these together, we can write:" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "id": "59c29856-c77e-48a1-9f17-15d4c58be588", "metadata": {}, "outputs": [ @@ -525,7 +569,7 @@ "source": [ "l = linear(x=10)\n", "t2 = times_two(x=l.outputs.x)\n", - "l > t2\n", + "l > t2 # Note: We can make arbitrarily long linear chains: l > t2 > something_else > another_node\n", "l.run()\n", "print(t2.inputs.x, t2.outputs.double)" ] @@ -546,7 +590,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, "id": "1a4e9693-0980-4435-aecc-3331d8b608dd", "metadata": {}, "outputs": [], @@ -558,7 +602,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 21, "id": "7c4d314b-33bb-4a67-bfb9-ed77fba3949c", "metadata": {}, "outputs": [ @@ -580,7 +624,7 @@ " return linspace\n", "\n", "lin = SingleValue(linspace_node)\n", - "lin.run()\n", + "lin()\n", "\n", "print(type(lin.outputs.linspace.value)) # Output is just what we expect\n", "print(lin[1:4]) # Gets items from the output\n", @@ -597,16 +641,19 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "id": "61ae572f-197b-4a60-8d3e-e19c1b9cc6e2", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "times_two (TimesTwo) output single-value: 4\n" - ] + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ @@ -622,9 +669,7 @@ "\n", "l = linear(x=2)\n", "t2 = times_two(x=l) # Just takes the whole `l` node!\n", - "l > t2\n", - "l.run()\n", - "print(t2)" + "t2.pull()" ] }, { @@ -632,29 +677,26 @@ "id": "b2e56a64-d053-4127-bb8c-069777c1c6b5", "metadata": {}, "source": [ - "Nodes can take input from multiple sources, and we can chain together these execution orders:" + "Nodes can take input from multiple sources, and -- although it's usually _useful_ to give each node its own variable -- we can even instantiate nodes inside the signature for initializing another node and call that node all at once! You won't have easy access to them, but this still just builds three nodes in memory, sets their data connections, and invokes a `pull` on the outermost (downstream-most) node, which automatically creates the execution flow and runs it:" ] }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "id": "6569014a-815b-46dd-8b47-4e1cd4584b3b", "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "array([0.49455794, 0.6789772 , 0.48470916, 0.43574953, 0.18030331,\n", - " 0.6059215 , 0.65871187, 0.42205006, 0.65062977, 0.5390317 ])" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" + "name": "stderr", + "output_type": "stream", + "text": [ + "/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" + ] }, { "data": { - "image/png": "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", + "image/png": "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ssrKyoL2/+c//6lrr71Wa9asUUpKin75y1/qoYce0rFjx664eAAA4Hy2wsj58+dVU1Oj3NzcgPbc3FwdPnw46JicnBydOXNGFRUVsixLX375pfbs2aM777wz9KoBAEDYsBVGWltb1d3drcTExID2xMRENTc3Bx2Tk5OjXbt2KT8/X7GxsUpKStJVV12ll19+ecDX6erqUnt7e8ADAACEp5AuYHW5XAHPLcvq19arrq5Oa9as0VNPPaWamhpVVlaqoaFBBQUFA/7+kpISeTyevkdycnIoZQIAAAdwWZZlDbbz+fPnNW7cOO3evVt33313X/vatWt18uRJHThwoN+YZcuWqbOzU7t37+5rO3jwoObNm6ezZ8/K6/X2G9PV1aWurq6+5+3t7UpOTpbf71d8fPyg3xwAADCnvb1dHo/nsp/ftlZGYmNjlZGRoaqqqoD2qqoq5eTkBB3z7bffKioq8GWio6MlXVxRCSYuLk7x8fEBDwAAEJ5sb9MUFhbqjTfe0Pbt21VfX6/169ersbGxb9tl48aNWr58eV//vLw8vfvuuyorK9Pp06d16NAhrVmzRllZWZo6derQvRMAAOBIMXYH5Ofnq62tTZs3b5bP51NaWpoqKio0ffp0SZLP5ws4c+T+++9XR0eH/vznP+v3v/+9rrrqKt1222169tlnh+5dAAAAx7J1zYgpg91zAgAAo8ewXDMCAAAw1AgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAo0IKI6WlpUpJSZHb7VZGRoaqq6sv2b+rq0ubNm3S9OnTFRcXp5kzZ2r79u0hFQwAAMJLjN0B5eXlWrdunUpLSzV37ly9+uqrWrhwoerq6nTNNdcEHbNkyRJ9+eWX2rZtm37605+qpaVFFy5cuOLiAQCA87ksy7LsDJgzZ47S09NVVlbW1zZr1iwtXrxYJSUl/fpXVlbq3nvv1enTp5WQkBBSke3t7fJ4PPL7/YqPjw/pdwAAgEDdPZaONpxTS0enpkxwKyslQdFRriH7/YP9/La1MnL+/HnV1NRow4YNAe25ubk6fPhw0DF79+5VZmamnnvuOf3lL3/R+PHjddddd+mPf/yjxo4dG3RMV1eXurq6At4MAAAYOpW1PhXvq5PP39nX5vW4VZSXqgVp3hGtxdY1I62treru7lZiYmJAe2Jiopqbm4OOOX36tA4ePKja2lq99957evHFF7Vnzx498sgjA75OSUmJPB5P3yM5OdlOmQAA4BIqa31avfN4QBCRpGZ/p1bvPK7KWt+I1hPSBawuV+ASjmVZ/dp69fT0yOVyadeuXcrKytKiRYv0wgsvaMeOHfruu++Cjtm4caP8fn/fo6mpKZQyAQDAj3T3WCreV6dg12j0thXvq1N3j62rOK6IrTAyadIkRUdH91sFaWlp6bda0svr9erqq6+Wx+Ppa5s1a5Ysy9KZM2eCjomLi1N8fHzAAwAAXLmjDef6rYj8kCXJ5+/U0YZzI1aTrTASGxurjIwMVVVVBbRXVVUpJycn6Ji5c+fq7Nmz+vrrr/vaPv30U0VFRWnatGkhlAwAAELV0jFwEAml31CwvU1TWFioN954Q9u3b1d9fb3Wr1+vxsZGFRQUSLq4xbJ8+fK+/vfdd58mTpyolStXqq6uTh9++KEef/xx/fa3vx3wAlYAADA8pkxwD2m/oWD7nJH8/Hy1tbVp8+bN8vl8SktLU0VFhaZPny5J8vl8amxs7Ov/k5/8RFVVVfrd736nzMxMTZw4UUuWLNEzzzwzdO8CAAAMSlZKgrwet5r9nUGvG3FJSvJcvM13pNg+Z8QEzhkBAGDo9N5NIykgkPTeilK2NH1Ibu8d7Oc3300DAECEWZDmVdnSdCV5ArdikjzuIQsidtjepgEAAM63IM2r+alJw3oC62ARRgAAiFDRUS5lz5xougy2aQAAgFmEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABGEUYAAIBRhBEAAGAUYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABGEUYAAIBRhBEAAGAUYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABGEUYAAIBRhBEAAGAUYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABGEUYAAIBRhBEAAGAUYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABGEUYAAIBRIYWR0tJSpaSkyO12KyMjQ9XV1YMad+jQIcXExOimm24K5WUBAEAYsh1GysvLtW7dOm3atEknTpzQvHnztHDhQjU2Nl5ynN/v1/Lly/U///M/IRcLAADCj8uyLMvOgDlz5ig9PV1lZWV9bbNmzdLixYtVUlIy4Lh7771X1113naKjo/W3v/1NJ0+eHPRrtre3y+PxyO/3Kz4+3k65AADAkMF+fttaGTl//rxqamqUm5sb0J6bm6vDhw8POO7NN9/UqVOnVFRUZOflAABABIix07m1tVXd3d1KTEwMaE9MTFRzc3PQMZ999pk2bNig6upqxcQM7uW6urrU1dXV97y9vd1OmQAAwEFCuoDV5XIFPLcsq1+bJHV3d+u+++5TcXGxrr/++kH//pKSEnk8nr5HcnJyKGUCAAAHsBVGJk2apOjo6H6rIC0tLf1WSySpo6NDx44d06OPPqqYmBjFxMRo8+bN+uijjxQTE6MPPvgg6Ots3LhRfr+/79HU1GSnTAAA4CC2tmliY2OVkZGhqqoq3X333X3tVVVV+tWvftWvf3x8vD7++OOAttLSUn3wwQfas2ePUlJSgr5OXFyc4uLi7JQGAAAcylYYkaTCwkItW7ZMmZmZys7O1muvvabGxkYVFBRIuriq8cUXX+itt95SVFSU0tLSAsZPmTJFbre7XzsAAIhMtsNIfn6+2tratHnzZvl8PqWlpamiokLTp0+XJPl8vsueOQIAANDL9jkjJnDOCAAAzjMs54wAAAAMNcIIAAAwijACAACMIowAAACjbN9NAwAwr7vH0tGGc2rp6NSUCW5lpSQoOqr/SdiAExBGAMBhKmt9Kt5XJ5+/s6/N63GrKC9VC9K8BisDQsM2DQA4SGWtT6t3Hg8IIpLU7O/U6p3HVVnrM1QZEDrCCAA4RHePpeJ9dQp2OFRvW/G+OnX3jPrjo4AAhBEAcIijDef6rYj8kCXJ5+/U0YZzI1cUMAQIIwDgEC0dAweRUPoBowVhBAAcYsoE95D2A0YLwggAOERWSoK8HrcGuoHXpYt31WSlJIxkWcAVI4wAgENER7lUlJcqSf0CSe/zorxUzhuB4xBGAMBBFqR5VbY0XUmewK2YJI9bZUvTOWcEjsShZwDgMAvSvJqfmsQJrAgbhBEAcKDoKJeyZ040XQYwJNimAQAARhFGAACAUYQRAABgFGEEAAAYxQWsgIN191jcUQHA8QgjgENV1vpUvK8u4IvTvB63ivJSOWsCgKOwTQM4UGWtT6t3Hu/3Da7N/k6t3nlclbU+Q5UBgH2EEcBhunssFe+rkxXkZ71txfvq1N0TrAcAjD6EEcBhjjac67ci8kOWJJ+/U0cbzo1cUQBwBQgjgMO0dAwcRELpBwCmEUYAh5kywX35Tjb6AYBphBHAYbJSEuT1uPt9hXwvly7eVZOVkjCSZQFAyAgjgMNER7lUlJcqSf0CSe/zorxUzhsB4BiEEcCBFqR5VbY0XUmewK2YJI9bZUvTOWcEgKNw6BngUAvSvJqfmsQJrIh4nETsfIQRwMGio1zKnjnRdBmAMZxEHB7YpgEAOBInEYcPwggAwHE4iTi8EEYAAI7DScThhTACAHAcTiIOL4QRAIDjcBJxeCGMAAAch5OIwwthBADgOJxEHF4IIwAAR+Ik4vDBoWcAAMfiJOLwQBgBADgaJxE7H9s0AADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAo/iiPDhSd4/Ft3QCQJggjMBxKmt9Kt5XJ5+/s6/N63GrKC9VC9K8BisDAISCbRo4SmWtT6t3Hg8IIpLU7O/U6p3HVVnrM1QZACBUhBE4RnePpeJ9dbKC/Ky3rXhfnbp7gvUAgMjT3WPpyKk2/e/JL3TkVNuo/feRbRo4xtGGc/1WRH7IkuTzd+powzllz5w4coUBwCjkpC1tVkbgGC0dAweRUPoBQLhy2pY2YQSOMWWCe0j7AUA4cuKWdkhhpLS0VCkpKXK73crIyFB1dfWAfd99913Nnz9fkydPVnx8vLKzs/X++++HXDAiV1ZKgrwetwa6gdeli0uQWSkJI1kWAIwqdra0RwvbYaS8vFzr1q3Tpk2bdOLECc2bN08LFy5UY2Nj0P4ffvih5s+fr4qKCtXU1OjWW29VXl6eTpw4ccXFI7JER7lUlJcqSf0CSe/zorxUzhsBENGcuKXtsizL1jrNnDlzlJ6errKysr62WbNmafHixSopKRnU77jxxhuVn5+vp556alD929vb5fF45Pf7FR8fb6dchCEnXZQFACPtyKk2/b/X/3nZfu88ePOwX+w/2M9vW3fTnD9/XjU1NdqwYUNAe25urg4fPjyo39HT06OOjg4lJAy8lN7V1aWurq6+5+3t7XbKRJhbkObV/NQkTmAFgCB6t7Sb/Z1BrxtxSUoaZVvatrZpWltb1d3drcTExID2xMRENTc3D+p3/OlPf9I333yjJUuWDNinpKREHo+n75GcnGynTESA6CiXsmdO1K9uulrZMycSRADgv5y4pR3SBawuV+AbsCyrX1sw77zzjp5++mmVl5drypQpA/bbuHGj/H5/36OpqSmUMgEAiEgL0rwqW5quJE/g3YVJHrfKlqaPui1tW9s0kyZNUnR0dL9VkJaWln6rJT9WXl6uVatWaffu3br99tsv2TcuLk5xcXF2SgMAAD/gpC1tWysjsbGxysjIUFVVVUB7VVWVcnJyBhz3zjvv6P7779fbb7+tO++8M7RKAQCALU7Z0rZ9HHxhYaGWLVumzMxMZWdn67XXXlNjY6MKCgokXdxi+eKLL/TWW29JuhhEli9frpdeekk333xz36rK2LFj5fF4hvCtAAAAJ7IdRvLz89XW1qbNmzfL5/MpLS1NFRUVmj59uiTJ5/MFnDny6quv6sKFC3rkkUf0yCOP9LWvWLFCO3bsuPJ3AAAAHM32OSMmcM4IAADOM9jPb76bBgAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABGEUYAAIBRhBEAAGAUYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABGEUYAAIBRhBEAAGAUYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABGEUYAAIBRhBEAAGAUYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgVIzpAgAAztfdY+lowzm1dHRqygS3slISFB3lMl0WHIIwAgC4IpW1PhXvq5PP39nX5vW4VZSXqgVpXoOVwSnYpgEAhKyy1qfVO48HBBFJavZ3avXO46qs9RmqDE5CGAEAhKS7x1LxvjpZQX7W21a8r07dPcF6AP+HMAIACMnRhnP9VkR+yJLk83fqaMO5kSsKjkQYAQCEpKVj4CASSj9ELsIIACAkUya4h7QfIhdhBAAQkqyUBHk9bg10A69LF++qyUpJGMmy4ECEEQBASKKjXCrKS5WkfoGk93lRXirnjeCyCCMAgJAtSPOqbGm6kjyBWzFJHrfKlqZzzggGhUPPAABXZEGaV/NTkziBFSGL2DDC0cUAMHSio1zKnjnRdBlwqIgMIxxdDADA6BFx14xwdDEAAKNLRIURji4GAGD0iagwwtHFAACMPhEVRji6GACA0SeiwghHFwMAMPpEVBjh6GIAAEafiAojHF0MAMDoE1IYKS0tVUpKitxutzIyMlRdXX3J/gcOHFBGRobcbrdmzJihV155JaRihwJHFwMAMLrYPvSsvLxc69atU2lpqebOnatXX31VCxcuVF1dna655pp+/RsaGrRo0SI9+OCD2rlzpw4dOqSHH35YkydP1j333DMkb8Iuji4GAGD0cFmWZetQjTlz5ig9PV1lZWV9bbNmzdLixYtVUlLSr/+TTz6pvXv3qr6+vq+toKBAH330kY4cOTKo12xvb5fH45Hf71d8fLydcgEAgCGD/fy2tU1z/vx51dTUKDc3N6A9NzdXhw8fDjrmyJEj/frfcccdOnbsmL7//vugY7q6utTe3h7wAAAA4clWGGltbVV3d7cSExMD2hMTE9Xc3Bx0THNzc9D+Fy5cUGtra9AxJSUl8ng8fY/k5GQ7ZQIAAAcJ6QJWlyvw2grLsvq1Xa5/sPZeGzdulN/v73s0NTWFUiYAAHAAWxewTpo0SdHR0f1WQVpaWvqtfvRKSkoK2j8mJkYTJwb/uum4uDjFxcXZKQ0AADiUrZWR2NhYZWRkqKqqKqC9qqpKOTk5QcdkZ2f3679//35lZmZqzJgxNssFAADhxvY2TWFhod544w1t375d9fX1Wr9+vRobG1VQUCDp4hbL8uXL+/oXFBTo888/V2Fhoerr67V9+3Zt27ZNjz322NC9CwAA4Fi2zxnJz89XW1ubNm/eLJ/Pp7S0NFVUVGj69OmSJJ/Pp8bGxr7+KSkpqqio0Pr167V161ZNnTpVW7ZsMXbGCAAAGF1snzNiAueMAADgPMNyzggAAMBQs71NY0Lv4g2HnwEA4By9n9uX24RxRBjp6OiQJA4/AwDAgTo6OuTxeAb8uSOuGenp6dHZs2c1YcKESx6u1t7eruTkZDU1NXFtySjFHI1+zJEzME+jH3N0cUWko6NDU6dOVVTUwFeGOGJlJCoqStOmTRt0//j4+IideKdgjkY/5sgZmKfRL9Ln6FIrIr24gBUAABhFGAEAAEaFVRiJi4tTUVER32szijFHox9z5AzM0+jHHA2eIy5gBQAA4SusVkYAAIDzEEYAAIBRhBEAAGAUYQQAABjlqDBSWlqqlJQUud1uZWRkqLq6+pL9Dxw4oIyMDLndbs2YMUOvvPLKCFUa2ezM07vvvqv58+dr8uTJio+PV3Z2tt5///0RrDYy2f1b6nXo0CHFxMTopptuGt4CIcn+PHV1dWnTpk2aPn264uLiNHPmTG3fvn2Eqo1Mdudo165dmj17tsaNGyev16uVK1eqra1thKodxSyH+Otf/2qNGTPGev311626ujpr7dq11vjx463PP/88aP/Tp09b48aNs9auXWvV1dVZr7/+ujVmzBhrz549I1x5ZLE7T2vXrrWeffZZ6+jRo9ann35qbdy40RozZox1/PjxEa48ctido15fffWVNWPGDCs3N9eaPXv2yBQbwUKZp7vuusuaM2eOVVVVZTU0NFj/+te/rEOHDo1g1ZHF7hxVV1dbUVFR1ksvvWSdPn3aqq6utm688UZr8eLFI1z56OOYMJKVlWUVFBQEtN1www3Whg0bgvZ/4oknrBtuuCGg7aGHHrJuvvnmYasR9ucpmNTUVKu4uHioS8N/hTpH+fn51h/+8AerqKiIMDIC7M7T3//+d8vj8VhtbW0jUR4s+3P0/PPPWzNmzAho27JlizVt2rRhq9EpHLFNc/78edXU1Cg3NzegPTc3V4cPHw465siRI/3633HHHTp27Ji+//77Yas1koUyTz/W09Ojjo4OJSQkDEeJES/UOXrzzTd16tQpFRUVDXeJUGjztHfvXmVmZuq5557T1Vdfreuvv16PPfaYvvvuu5EoOeKEMkc5OTk6c+aMKioqZFmWvvzyS+3Zs0d33nnnSJQ8qjnii/JaW1vV3d2txMTEgPbExEQ1NzcHHdPc3By0/4ULF9Ta2iqv1zts9UaqUObpx/70pz/pm2++0ZIlS4ajxIgXyhx99tln2rBhg6qrqxUT44h/MhwvlHk6ffq0Dh48KLfbrffee0+tra16+OGHde7cOa4bGQahzFFOTo527dql/Px8dXZ26sKFC7rrrrv08ssvj0TJo5ojVkZ6uVyugOeWZfVru1z/YO0YWnbnqdc777yjp59+WuXl5ZoyZcpwlQcNfo66u7t13333qbi4WNdff/1IlYf/svO31NPTI5fLpV27dikrK0uLFi3SCy+8oB07drA6MozszFFdXZ3WrFmjp556SjU1NaqsrFRDQ4MKCgpGotRRzRH/zZk0aZKio6P7pc2WlpZ+qbRXUlJS0P4xMTGaOHHisNUayUKZp17l5eVatWqVdu/erdtvv304y4xodueoo6NDx44d04kTJ/Too49KuvihZ1mWYmJitH//ft12220jUnskCeVvyev16uqrrw74uvZZs2bJsiydOXNG11133bDWHGlCmaOSkhLNnTtXjz/+uCTp5z//ucaPH6958+bpmWeeiegVe0esjMTGxiojI0NVVVUB7VVVVcrJyQk6Jjs7u1///fv3KzMzU2PGjBm2WiNZKPMkXVwRuf/++/X222+zdzrM7M5RfHy8Pv74Y508ebLvUVBQoJ/97Gc6efKk5syZM1KlR5RQ/pbmzp2rs2fP6uuvv+5r+/TTTxUVFaVp06YNa72RKJQ5+vbbbxUVFfixGx0dLen/Vu4jlqkrZ+3qvYVq27ZtVl1dnbVu3Tpr/Pjx1n/+8x/Lsixrw4YN1rJly/r6997au379equurs7atm0bt/aOALvz9Pbbb1sxMTHW1q1bLZ/P1/f46quvTL2FsGd3jn6Mu2lGht156ujosKZNm2b9+te/tj755BPrwIED1nXXXWc98MADpt5C2LM7R2+++aYVExNjlZaWWqdOnbIOHjxoZWZmWllZWabewqjhmDBiWZa1detWa/r06VZsbKyVnp5uHThwoO9nK1assG655ZaA/v/4xz+sX/ziF1ZsbKx17bXXWmVlZSNccWSyM0+33HKLJanfY8WKFSNfeASx+7f0Q4SRkWN3nurr663bb7/dGjt2rDVt2jSrsLDQ+vbbb0e46shid462bNlipaamWmPHjrW8Xq/1m9/8xjpz5swIVz36uCwr0teGAACASY64ZgQAAIQvwggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACj/j+E4OcogDUlLwAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -676,11 +718,10 @@ " fig = plt.scatter(x, y)\n", " return fig\n", "\n", - "x = noise(length=10)\n", - "y = noise(length=10)\n", - "f = plot(x=x, y=y)\n", - "x > y > f\n", - "x()" + "plot_output = plot(\n", + " x=noise(length=10),\n", + " y=noise(length=10),\n", + ")()" ] }, { @@ -694,7 +735,7 @@ "We offer a formal way to group these objects together as a `Workflow(Node)` object.\n", "`Workflow` also offers us a single point of entry to the codebase -- i.e. most of the time you shouldn't need the node imports used above, because the decorators are available right on the workflow class.\n", "\n", - "We will also see here that we can our node output channels using the `output_labels: Optional[str | list[str] | tuple[str]` kwarg, in case they don't have a convenient name to start with.\n", + "We will also see here that we can rename our node output channels using the `output_labels: Optional[str | list[str] | tuple[str]` kwarg, in case they don't have a convenient name to start with.\n", "This way we can always have convenient dot-based access (and tab completion) instead of having to access things by string-based keys.\n", "\n", "Finally, when a workflow is run, unless its `automate_execution` flag has been set to `False` or the data connections form a cyclic graph, it will _automatically_ build the necessary run signals! That means for all directed acyclic graph (DAG) workflows, all we typically need to worry about is the data connections." @@ -710,7 +751,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 24, "id": "1cd000bd-9b24-4c39-9cac-70a3291d0660", "metadata": {}, "outputs": [], @@ -737,7 +778,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 25, "id": "7964df3c-55af-4c25-afc5-9e07accb606a", "metadata": {}, "outputs": [ @@ -778,7 +819,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 26, "id": "809178a5-2e6b-471d-89ef-0797db47c5ad", "metadata": {}, "outputs": [ @@ -832,7 +873,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 27, "id": "52c48d19-10a2-4c48-ae81-eceea4129a60", "metadata": {}, "outputs": [ @@ -842,7 +883,7 @@ "{'ay': 3, 'a + b + 2': 7}" ] }, - "execution_count": 26, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -852,6 +893,14 @@ "out" ] }, + { + "cell_type": "markdown", + "id": "a229a66b-54f0-4d79-a16f-669c5f755587", + "metadata": {}, + "source": [ + "Note: Workflows are the \"parent-most\" node, so even though `__call__` is still invoking a `pull`, the \"run all upstream data dependencies\" part of \"run all upstream data dependencies then run yourself\" gets skipped trivially -- workflows can't have siblings or parents so there are no dependencies to run! Thus `__call__` is effectively just a `run`." + ] + }, { "cell_type": "markdown", "id": "e3f4b51b-7c28-47f7-9822-b4755e12bd4d", @@ -862,7 +911,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 28, "id": "bb35ba3e-602d-4c9c-b046-32da9401dd1c", "metadata": {}, "outputs": [ @@ -872,7 +921,7 @@ "(7, 3)" ] }, - "execution_count": 27, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -891,7 +940,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 29, "id": "2b0d2c85-9049-417b-8739-8a8432a1efbe", "metadata": {}, "outputs": [ @@ -1209,10 +1258,10 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 28, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } @@ -1239,14 +1288,14 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 30, "id": "ae500d5e-e55b-432c-8b5f-d5892193cdf5", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "4f07c83c4a694b76847e9060c58c00d0", + "model_id": "c46776a009974c03934aeea1cb8be1ce", "version_major": 2, "version_minor": 0 }, @@ -1265,10 +1314,10 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 29, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" }, @@ -1311,7 +1360,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 31, "id": "2114d0c3-cdad-43c7-9ffa-50c36d56d18f", "metadata": {}, "outputs": [ @@ -1330,27 +1379,27 @@ "clusterwith_prebuilt\n", "\n", "with_prebuilt: Workflow\n", - "\n", - "clusterwith_prebuiltOutputs\n", + "\n", + "clusterwith_prebuiltInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterwith_prebuiltInputs\n", + "\n", + "clusterwith_prebuiltOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Outputs\n", "\n", "\n", "\n", @@ -1519,10 +1568,10 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 30, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } @@ -1551,7 +1600,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 32, "id": "c71a8308-f8a1-4041-bea0-1c841e072a6d", "metadata": {}, "outputs": [], @@ -1561,7 +1610,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 33, "id": "2b9bb21a-73cd-444e-84a9-100e202aa422", "metadata": {}, "outputs": [ @@ -1579,7 +1628,7 @@ "13" ] }, - "execution_count": 32, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } @@ -1618,7 +1667,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 34, "id": "3668f9a9-adca-48a4-84ea-13add965897c", "metadata": {}, "outputs": [ @@ -1628,7 +1677,7 @@ "{'intermediate': 102, 'plus_three': 103}" ] }, - "execution_count": 33, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } @@ -1666,7 +1715,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 35, "id": "9aaeeec0-5f88-4c94-a6cc-45b56d2f0111", "metadata": {}, "outputs": [], @@ -1696,7 +1745,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 36, "id": "a832e552-b3cc-411a-a258-ef21574fc439", "metadata": {}, "outputs": [], @@ -1723,7 +1772,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 37, "id": "b764a447-236f-4cb7-952a-7cba4855087d", "metadata": {}, "outputs": [ @@ -1742,159 +1791,159 @@ "clusterphase_preference\n", "\n", "phase_preference: Workflow\n", - "\n", - "clusterphase_preferenceInputs\n", + "\n", + "clusterphase_preferencemin_phase2\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", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterphase_preferenceOutputs\n", + "\n", + "clusterphase_preferencemin_phase2Outputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", - "\n", - "clusterphase_preferenceelement\n", + "\n", + "clusterphase_preferencecompare\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "element: UserInput\n", + "\n", + "compare: PerAtomEnergyDifference\n", "\n", - "\n", - "clusterphase_preferenceelementInputs\n", + "\n", + "clusterphase_preferencecompareInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterphase_preferenceelementOutputs\n", + "\n", + "clusterphase_preferencecompareOutputs\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", + "Outputs\n", "\n", - "\n", - "clusterphase_preferencemin_phase1Inputs\n", + "\n", + "clusterphase_preferenceInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterphase_preferencemin_phase1Outputs\n", + "\n", + "clusterphase_preferenceOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", - "\n", - "clusterphase_preferencemin_phase2\n", + "\n", + "clusterphase_preferenceelement\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", - "min_phase2: LammpsMinimize\n", + "\n", + "element: UserInput\n", "\n", - "\n", - "clusterphase_preferencemin_phase2Inputs\n", + "\n", + "clusterphase_preferenceelementInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterphase_preferencemin_phase2Outputs\n", + "\n", + "clusterphase_preferenceelementOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", - "\n", - "clusterphase_preferencecompare\n", + "\n", + "clusterphase_preferencemin_phase1\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", - "compare: PerAtomEnergyDifference\n", + "\n", + "min_phase1: LammpsMinimize\n", "\n", - "\n", - "clusterphase_preferencecompareInputs\n", + "\n", + "clusterphase_preferencemin_phase1Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterphase_preferencecompareOutputs\n", + "\n", + "clusterphase_preferencemin_phase1Outputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Outputs\n", "\n", "\n", "\n", @@ -2121,192 +2170,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", @@ -2383,74 +2432,74 @@ "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__cells\n", - "\n", - "min_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", + "min_phase2__calc__displacements\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__energy_tot\n", - "\n", - "min_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", + "min_phase2__calc__force_max\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__forces\n", - "\n", - "min_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", + "min_phase2__calc__indices\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__positions\n", - "\n", - "min_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", + "min_phase2__calc__pressures\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__steps\n", - "\n", - "min_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", + "min_phase2__calc__total_displacements\n", "\n", "\n", "\n", "clusterphase_preferenceOutputsmin_phase2__calc__unwrapped_positions\n", - "\n", - "min_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", + "min_phase2__calc__volume\n", "\n", "\n", "\n", @@ -2745,28 +2794,28 @@ "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__cells\n", - "\n", - "calc__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", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__displacements\n", - "\n", - "calc__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", "\n", "\n", "\n", @@ -2790,132 +2839,132 @@ "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__energy_tot\n", - "\n", - "calc__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", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__force_max\n", - "\n", - "calc__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", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__forces\n", - "\n", - "calc__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", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__indices\n", - "\n", - "calc__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", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__positions\n", - "\n", - "calc__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", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__pressures\n", - "\n", - "calc__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", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__steps\n", - "\n", - "calc__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", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__total_displacements\n", - "\n", - "calc__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", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__unwrapped_positions\n", - "\n", - "calc__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", "\n", "\n", "\n", "clusterphase_preferencemin_phase2Outputscalc__volume\n", - "\n", - "calc__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", "\n", "\n", "\n", @@ -2947,10 +2996,10 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 36, + "execution_count": 37, "metadata": {}, "output_type": "execute_result" } @@ -2961,7 +3010,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 38, "id": "b51bef25-86c5-4d57-80c1-ab733e703caf", "metadata": {}, "outputs": [ @@ -2982,7 +3031,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 39, "id": "091e2386-0081-436c-a736-23d019bd9b91", "metadata": {}, "outputs": [ @@ -3023,7 +3072,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 40, "id": "4cdffdca-48d3-4486-9045-48102c7e5f31", "metadata": {}, "outputs": [ @@ -3061,7 +3110,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 41, "id": "ed4a3a22-fc3a-44c9-9d4f-c65bc1288889", "metadata": {}, "outputs": [ @@ -3091,7 +3140,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 42, "id": "5a985cbf-c308-4369-9223-b8a37edb8ab1", "metadata": {}, "outputs": [ @@ -3119,6 +3168,22 @@ "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": "e5e75718-137f-43e6-b484-5ebe4ab22bf8", + "metadata": {}, + "source": [ + "Now that we have nested macros, we can finally discuss the subtle difference between `__call__` and `pull`:\n", + "\n", + "Each `Macro` instance is its own little walled garden, where it's child nodes have no connections apart from those to other children of the same macro (you can forceably change this, since we're all adults here, but it won't happen by default and isn't recommended). Under the hood this is accomplished by the macro IO \"linking\" itself to its childrens' IO, so that updates to macro input values are always immediately propagated to children, and macro output gets synchronized with its childrens' output at the end of every run. Because of this we can think of these children all having the same \"scope\", i.e. siblings among the same parent.\n", + "\n", + "`pull` has a keyword argument to determine whether upstream data dependencies are restricted to be _in scope_, or if the parent node (if any) should also consider all _its_ data dependencies as well, and so on up until we hit the parent-most macro or workflow.\n", + "\n", + "For `pull` this parameter defaults to `False`, so that the pull stops at the parent node. For `__call__` it defaults to `True`, so that the search for data dependencies punches right through parents and all the way up. The danger is that this might be expensive if there's an costly node somewhere in the dependency!\n", + "\n", + "Note that the entire \"pull\" paradigm does currently play nicely with remote execution. If some of your nodes have an executor specified, you will need to `.run` your graph (or `__call__` a `Workflow` if that's your parent-most object)." + ] + }, { "cell_type": "markdown", "id": "f447531e-3e8c-4c7e-a579-5f9c56b75a5b", @@ -3136,10 +3201,11 @@ "source": [ "## Parallelization\n", "\n", - "You can currently run _some_ nodes (namely, `Function` nodes that don't take `self` as an argument) in a background process by setting an `executor` of the right type.\n", - "Cf. the `Workflow` class tests in the source code for an example.\n", + "You can currently run nodes in a single-core background process by setting that node's `executor` to `True`. The plan is to eventually lean on `pympipool` for more powerful executors that allow for multiple cores, interaction with HPC clusters, etc. We may also leverage the `Submitter` in `pyiron_contrib.tinybase` so that multiple nodes can lean on the same resources.\n", "\n", - "Right now our treatment of DAGs is quite rudimentary, and the data flow is (unless cyclic) converted into a _linear_ execution pattern. \n", + "Unfortunately, _nested_ executors are not yet working. So if you set a macro to use an executor, none of its (grand...)children may specify an executor.\n", + "\n", + "Note also that right now our treatment of DAGs is quite rudimentary, and the data flow is (unless cyclic) converted into a _linear_ execution pattern. \n", "This is practical and robust, but highly inefficient when combined with nodes that can run in parallel, i.e. with \"executors\".\n", "Going forward, we will exploit the same infrastructure of data flow DAGs and run signals to build up more sophisticated execution patterns which support parallelization." ] @@ -3151,9 +3217,9 @@ "source": [ "## Serialization and node libraries\n", "\n", - "Serialization doesn't exist yet.\n", + "Serialization for storage doesn't exist yet.\n", "\n", - "What you _can_ do is `register` new lists of nodes (including macros) with the workflow, so feel free to build up your own `.py` files containing nodes you like to use for easy re-use. Registration is now discussed in the main body of the notebook, but the API may change significantly going forward.\n", + "What you _can_ do is `register` new modules that have a list of nodes (including macros) with the workflow, so feel free to build up your own `.py` files containing nodes you like to use for easy re-use. Registration is now discussed in the main body of the notebook, but the API may change significantly going forward.\n", "\n", "Serialization of workflows is still forthcoming, while for node registration flexibility and documentation is forthcoming but the basics are here already." ] @@ -3181,7 +3247,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 43, "id": "0b373764-b389-4c24-8086-f3d33a4f7fd7", "metadata": {}, "outputs": [ @@ -3195,7 +3261,7 @@ " 17.230249999999995]" ] }, - "execution_count": 42, + "execution_count": 43, "metadata": {}, "output_type": "execute_result" } @@ -3232,7 +3298,7 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 44, "id": "0dd04b4c-e3e7-4072-ad34-58f2c1e4f596", "metadata": {}, "outputs": [ @@ -3291,7 +3357,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 45, "id": "2dfb967b-41ac-4463-b606-3e315e617f2a", "metadata": {}, "outputs": [ @@ -3315,7 +3381,7 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 46, "id": "2e87f858-b327-4f6b-9237-c8a557f29aeb", "metadata": {}, "outputs": [ @@ -3323,8 +3389,18 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.012 <= 0.2\n", - "Finally 0.012\n" + "0.639 > 0.2\n", + "0.481 > 0.2\n", + "0.582 > 0.2\n", + "0.213 > 0.2\n", + "0.829 > 0.2\n", + "0.826 > 0.2\n", + "0.401 > 0.2\n", + "0.929 > 0.2\n", + "0.251 > 0.2\n", + "0.525 > 0.2\n", + "0.087 <= 0.2\n", + "Finally 0.087\n" ] } ], From ba3e970b7596f610e6e4390a0bf80739c9d8918b Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Nov 2023 14:03:20 -0700 Subject: [PATCH 35/35] Fix Function docstring comments on self Which is currently not allowed at all --- pyiron_workflow/function.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index dfb633a0..3face365 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -301,16 +301,7 @@ class Function(Node): Comments: - If you use the function argument `self` in the first position, the - whole node object is inserted there: - - >>> def with_self(self, x): - >>> ... - >>> return x - - For this function, you don't have the freedom to choose `self`, because - pyiron automatically sets the node object there (which is also the - reason why you do not see `self` in the list of inputs). + Using the `self` argument for function nodes is not currently supported. """ def __init__(