From 4b3afa3b86a419bfd8fc125e31905be0c9b30aa4 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 7 Dec 2023 11:28:06 -0800 Subject: [PATCH 01/10] Purge owned creators and remove their promise --- pyiron_workflow/composite.py | 77 +----------------------------------- 1 file changed, 1 insertion(+), 76 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 2f86f9f7a..3d67a4d14 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -6,7 +6,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from functools import partial, wraps +from functools import wraps from typing import Literal, Optional, TYPE_CHECKING from bidict import bidict @@ -37,7 +37,6 @@ class Composite(Node, ABC): - From the instance level, created nodes get the instance as their parent - Child nodes... - Can be added by... - - Creating them from the creator on a composite _instance_ - Passing a node instance to the adding method - Setting the composite instance as the node's parent at node instantiation - Assigning a node instance as an attribute @@ -120,8 +119,6 @@ def __init__( self.outputs_map = outputs_map self.nodes: DotDict[str, Node] = DotDict() self.starting_nodes: list[Node] = [] - self._creator = self.create - self.create = self._owned_creator # Override the create method from the class @property def inputs_map(self) -> bidict | None: @@ -164,15 +161,6 @@ def deactivate_strict_hints(self): for node in self: node.deactivate_strict_hints() - @property - def _owned_creator(self): - """ - A misdirection so that the `create` method behaves differently on the class - and on instances (in the latter case, created nodes should get the instance as - their parent). - """ - return OwnedCreator(self, self._creator) - def to_dict(self): return { "label": self.label, @@ -570,66 +558,3 @@ def __dir__(self): def color(self) -> str: """For drawing the graph""" return SeabornColors.brown - - -class OwnedCreator: - """ - A creator that overrides the `parent` arg of all accessed nodes to its own parent. - - Necessary so that `Workflow.create.Function(...)` returns an unowned function node, - while `some_workflow_instance.create.Function(...)` returns a function node owned - by the workflow instance. - """ - - def __init__(self, parent: Composite, creator: Creator): - self._parent = parent - self._creator = creator - - def __getattr__(self, item): - value = getattr(self._creator, item) - - try: - is_node_class = issubclass(value, Node) - except TypeError: - # issubclass complains if the value isn't even a class - is_node_class = False - - if is_node_class: - value = partial(value, parent=self._parent) - elif isinstance(value, NodePackage): - value = OwnedNodePackage(self._parent, value) - - return value - - def __getstate__(self): - # Compatibility with python <3.11 - return self.__dict__ - - def __setstate__(self, state): - # Because we override getattr, we need to use __dict__ assignment directly in - # __setstate__ - self.__dict__["_parent"] = state["_parent"] - self.__dict__["_creator"] = state["_creator"] - - -class OwnedNodePackage: - """ - A wrapper for node packages so that accessed node classes can have their parent - value automatically filled. - """ - - def __init__(self, parent: Composite, node_package: NodePackage): - self._parent = parent - self._node_package = node_package - - def __getattr__(self, item): - value = getattr(self._node_package, item) - if issubclass(value, Node): - value = partial(value, parent=self._parent) - return value - - def __getstate__(self): - return self.__dict__ - - def __setstate__(self, state): - self.__dict__ = state From 2b548a75f053baf33d4d9e48aa51e145d8e392a9 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 7 Dec 2023 11:31:13 -0800 Subject: [PATCH 02/10] Update Workflow docs --- pyiron_workflow/workflow.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 2d06147e5..6e8f01932 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -54,25 +54,22 @@ class Workflow(Composite): We allow adding nodes to workflows in five equivalent ways: >>> from pyiron_workflow.workflow import Workflow >>> + >>> @Workflow.wrap_as.single_value_node() >>> def fnc(x=0): ... return x + 1 >>> >>> # (1) As *args at instantiation - >>> n1 = Workflow.create.Function(fnc, label="n1") + >>> n1 = fnc(label="n1") >>> wf = Workflow("my_workflow", n1) >>> >>> # (2) Being passed to the `add` method - >>> added = wf.add(Workflow.create.Function(fnc, label="n2")) + >>> n2 = wf.add(fnc(label="n2")) >>> - >>> # (3) Calling `create` from the _workflow instance_ that will own the node - >>> added = wf.create.Function(fnc, label="n3") # Instantiating from add + >>> # (3) By attribute assignment + >>> wf.n3 = fnc(label="anyhow_n3_gets_used") >>> - >>> # (4) By attribute assignment (here the node can be created from the - >>> # workflow class or instance and the end result is the same - >>> wf.n4 = wf.create.Function(fnc, label="anyhow_n4_gets_used") - >>> - >>> # (5) By creating from the workflow class but specifying the parent kwarg - >>> added = Workflow.create.Function(fnc, label="n5", parent=wf) + >>> # (4) By creating from the workflow class but specifying the parent kwarg + >>> n4 = fnc(label="n4", parent=wf) By default, the node naming scheme is strict, so if you try to add a node to a label that already exists, you will get an error. This behaviour can be changed @@ -80,9 +77,9 @@ class Workflow(Composite): bool to this property. When deactivated, repeated assignments to the same label just get appended with an index: >>> wf.strict_naming = False - >>> wf.my_node = wf.create.Function(fnc, x=0) - >>> wf.my_node = wf.create.Function(fnc, x=1) - >>> wf.my_node = wf.create.Function(fnc, x=2) + >>> wf.my_node = fnc(x=0) + >>> wf.my_node = fnc(x=1) + >>> wf.my_node = fnc(x=2) >>> print(wf.my_node.inputs.x, wf.my_node0.inputs.x, wf.my_node1.inputs.x) 0 1 2 From 5ea9b4ecaf637deeb26ef9e4a1ba88bd44157e23 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 7 Dec 2023 11:34:01 -0800 Subject: [PATCH 03/10] Use the registration shortcut Minor and unrelated to what I'm doing, but I found it in my ctrl+F --- tests/integration/test_workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_workflow.py b/tests/integration/test_workflow.py index 837be4481..983410649 100644 --- a/tests/integration/test_workflow.py +++ b/tests/integration/test_workflow.py @@ -191,7 +191,7 @@ def test_executor_and_creator_interaction(self): C.f. `pyiron_workflow.function._wrapper_factory` for more detail. """ wf = Workflow("depickle") - wf.create.register("demo", "static.demo_nodes") + wf.register("demo", "static.demo_nodes") wf.before_pickling = wf.create.demo.OptionallyAdd(1) wf.before_pickling.executor = wf.create.Executor() From b7b7e67d30a9d480c3b780c8cdbdfb27ebdd1155 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 7 Dec 2023 11:46:37 -0800 Subject: [PATCH 04/10] Manually parent nodes --- pyiron_workflow/meta.py | 7 +++++-- pyiron_workflow/workflow.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pyiron_workflow/meta.py b/pyiron_workflow/meta.py index a861d5579..cc5040c65 100644 --- a/pyiron_workflow/meta.py +++ b/pyiron_workflow/meta.py @@ -138,7 +138,10 @@ def make_loop(macro): # Or distribute the same input to each node equally else: interface = macro.create.standard.UserInput( - label=label, output_labels=label, user_input=inp.default + label=label, + output_labels=label, + user_input=inp.default, + parent=macro, ) for body_node in body_nodes: body_node.inputs[label] = interface @@ -287,7 +290,7 @@ def while_loop( def make_loop(macro): body_node = macro.add(loop_body_class(label=loop_body_class.__name__)) condition_node = macro.add(condition_class(label=condition_class.__name__)) - switch = macro.create.standard.If(label="switch") + switch = macro.create.standard.If(label="switch", parent=macro) switch.inputs.condition = condition_node for out_n, out_c, in_n, in_c in internal_connection_map: diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 6e8f01932..688126aae 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -55,7 +55,7 @@ class Workflow(Composite): >>> from pyiron_workflow.workflow import Workflow >>> >>> @Workflow.wrap_as.single_value_node() - >>> def fnc(x=0): + ... def fnc(x=0): ... return x + 1 >>> >>> # (1) As *args at instantiation From bab48f3bd04d5c0aa2450d04cc40514f76f1c8a3 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 7 Dec 2023 11:46:43 -0800 Subject: [PATCH 05/10] Update tests --- tests/unit/test_composite.py | 37 +++++++++++++++++------------------- tests/unit/test_workflow.py | 8 ++++---- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/tests/unit/test_composite.py b/tests/unit/test_composite.py index ed6d16414..1f4faa71d 100644 --- a/tests/unit/test_composite.py +++ b/tests/unit/test_composite.py @@ -69,37 +69,37 @@ def test_creator_access_and_registration(self): self.comp.register("demo", "static.demo_nodes") # Test invocation - self.comp.create.demo.OptionallyAdd(label="by_add") + self.comp.add(self.comp.create.demo.OptionallyAdd(label="by_add")) # Test invocation with attribute assignment self.comp.by_assignment = self.comp.create.demo.OptionallyAdd() - node = AComposite.create.demo.OptionallyAdd() + node = self.comp.create.demo.OptionallyAdd() self.assertSetEqual( set(self.comp.nodes.keys()), set(["by_add", "by_assignment"]), - msg=f"Expected one node label generated automatically from the class and " - f"the other from the attribute assignment, but got {self.comp.nodes.keys()}" + msg=f"Expected one node label generated automatically from the add call " + f"and the other from the attribute assignment, but got " + f"{self.comp.nodes.keys()}" ) self.assertIsNone( node.parent, - msg="Creating from the class directly should not parent the created nodes" + msg="Just creating should not parent the created nodes" ) def test_node_addition(self): # Validate the four ways to add a node self.comp.add(Composite.create.Function(plus_one, label="foo")) - self.comp.create.Function(plus_one, label="bar") self.comp.baz = self.comp.create.Function(plus_one, label="whatever_baz_gets_used") Composite.create.Function(plus_one, label="qux", parent=self.comp) self.assertListEqual( list(self.comp.nodes.keys()), - ["foo", "bar", "baz", "qux"], + ["foo", "baz", "qux"], msg="Expected every above syntax to add a node OK" ) self.comp.boa = self.comp.qux self.assertListEqual( list(self.comp.nodes.keys()), - ["foo", "bar", "baz", "boa"], + ["foo", "baz", "boa"], msg="Reassignment should remove the original instance" ) @@ -173,9 +173,6 @@ def test_label_uniqueness(self): with self.assertRaises(AttributeError, msg="We have 'foo' at home"): self.comp.add(self.comp.create.Function(plus_one, label="foo")) - with self.assertRaises(AttributeError, msg="We have 'foo' at home"): - self.comp.create.Function(plus_one, label="foo") - with self.assertRaises( AttributeError, msg="The provided label is ok, but then assigning to baz should give " @@ -221,8 +218,8 @@ def test_label_uniqueness(self): def test_singular_ownership(self): comp1 = AComposite("one") - comp1.create.Function(plus_one, label="node1") - node2 = AComposite.create.Function( + comp1.node1 = comp1.create.Function(plus_one) + node2 = comp1.create.Function( plus_one, label="node2", parent=comp1, x=comp1.node1.outputs.y ) self.assertTrue(node2.connected, msg="Sanity check that node connection works") @@ -413,9 +410,9 @@ def test_length(self): ) def test_run(self): - self.comp.create.SingleValue(plus_one, label="n1", x=0) - self.comp.create.SingleValue(plus_one, label="n2", x=self.comp.n1) - self.comp.create.SingleValue(plus_one, label="n3", x=42) + self.comp.n1 = self.comp.create.SingleValue(plus_one, x=0) + self.comp.n2 = self.comp.create.SingleValue(plus_one, x=self.comp.n1) + self.comp.n3 = self.comp.create.SingleValue(plus_one, x=42) self.comp.n1 >> self.comp.n2 self.comp.starting_nodes = [self.comp.n1] @@ -433,9 +430,9 @@ def test_run(self): def test_set_run_signals_to_dag(self): # Like the run test, but manually invoking this first - self.comp.create.SingleValue(plus_one, label="n1", x=0) - self.comp.create.SingleValue(plus_one, label="n2", x=self.comp.n1) - self.comp.create.SingleValue(plus_one, label="n3", x=42) + self.comp.n1 = self.comp.create.SingleValue(plus_one, x=0) + self.comp.n2 = self.comp.create.SingleValue(plus_one, x=self.comp.n1) + self.comp.n3 = self.comp.create.SingleValue(plus_one, x=42) self.comp.set_run_signals_to_dag_execution() self.comp.run() self.assertEqual( @@ -465,7 +462,7 @@ def test_return(self): self.comp.n1 = Composite.create.SingleValue(plus_one, x=0) not_dottable_string = "can't dot this" not_dottable_name_node = self.comp.create.SingleValue( - plus_one, x=42, label=not_dottable_string + plus_one, x=42, label=not_dottable_string, parent=self.comp ) self.comp.starting_nodes = [self.comp.n1, not_dottable_name_node] out = self.comp.run() diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index e5aab2a2f..7b0d64c1f 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -22,9 +22,9 @@ def setUpClass(cls) -> None: def test_io(self): wf = Workflow("wf") - wf.create.Function(plus_one, label="n1") - wf.create.Function(plus_one, label="n2") - wf.create.Function(plus_one, label="n3") + wf.n1 = wf.create.Function(plus_one) + wf.n2 = wf.create.Function(plus_one) + wf.n3 = wf.create.Function(plus_one) inp = wf.inputs inp_again = wf.inputs @@ -34,7 +34,7 @@ def test_io(self): n_in = len(wf.inputs) n_out = len(wf.outputs) - wf.create.Function(plus_one, label="n4") + wf.n4 = wf.create.Function(plus_one) self.assertEqual( n_in + 1, len(wf.inputs), msg="Workflow IO should be drawn from its nodes" ) From ae6b74f4b02da52364d50c25f826ee7c30e379e3 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 7 Dec 2023 11:59:42 -0800 Subject: [PATCH 06/10] Refactor: rename Composite.add to add_node --- pyiron_workflow/composite.py | 8 ++++---- pyiron_workflow/meta.py | 6 +++--- pyiron_workflow/node.py | 2 +- pyiron_workflow/workflow.py | 4 ++-- tests/unit/test_composite.py | 14 +++++++------- tests/unit/test_macro.py | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 3d67a4d14..7570999b1 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -87,7 +87,7 @@ class Composite(Node, ABC): wrap_as (Wrappers): A tool for accessing node-creating decorators Methods: - add(node: Node): Add the node instance to this subgraph. + add_node(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`. (de)activate_strict_hints(): Recursively (de)activate strict type hints. @@ -292,7 +292,7 @@ def _build_inputs(self) -> Inputs: def _build_outputs(self) -> Outputs: return self._build_io("outputs", self.outputs_map) - def add(self, node: Node, label: Optional[str] = None) -> None: + def add_node(self, node: Node, label: Optional[str] = None) -> None: """ Assign a node to the parent. Optionally provide a new label for that node. @@ -445,7 +445,7 @@ def replace(self, owned_node: Node | str, replacement: Node | type[Node]) -> Nod is_starting_node = owned_node in self.starting_nodes self.remove(owned_node) replacement.label, owned_node.label = owned_node.label, replacement.label - self.add(replacement) + self.add_node(replacement) if is_starting_node: self.starting_nodes.append(replacement) @@ -518,7 +518,7 @@ def executor_shutdown(self, wait=True, *, cancel_futures=False): def __setattr__(self, key: str, node: Node): if isinstance(node, Node) and key != "_parent": - self.add(node, label=key) + self.add_node(node, label=key) elif ( isinstance(node, type) and issubclass(node, Node) diff --git a/pyiron_workflow/meta.py b/pyiron_workflow/meta.py index cc5040c65..b56b40b3a 100644 --- a/pyiron_workflow/meta.py +++ b/pyiron_workflow/meta.py @@ -111,7 +111,7 @@ def make_loop(macro): # Parallelize over body nodes for n in range(length): body_nodes.append( - macro.add(loop_body_class(label=f"{loop_body_class.__name__}_{n}")) + macro.add_node(loop_body_class(label=f"{loop_body_class.__name__}_{n}")) ) # Make input interface @@ -288,8 +288,8 @@ def while_loop( """ def make_loop(macro): - body_node = macro.add(loop_body_class(label=loop_body_class.__name__)) - condition_node = macro.add(condition_class(label=condition_class.__name__)) + body_node = macro.add_node(loop_body_class(label=loop_body_class.__name__)) + condition_node = macro.add_node(condition_class(label=condition_class.__name__)) switch = macro.create.standard.If(label="switch", parent=macro) switch.inputs.condition = condition_node diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 8065c4a9a..1c5fdb2f2 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -234,7 +234,7 @@ def __init__( self.label: str = label self._parent = None if parent is not None: - parent.add(self) + parent.add_node(self) self.running = False self.failed = False self.signals = self._build_signal_channels() diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 688126aae..ed4ae4c44 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -63,7 +63,7 @@ class Workflow(Composite): >>> wf = Workflow("my_workflow", n1) >>> >>> # (2) Being passed to the `add` method - >>> n2 = wf.add(fnc(label="n2")) + >>> n2 = wf.add_node(fnc(label="n2")) >>> >>> # (3) By attribute assignment >>> wf.n3 = fnc(label="anyhow_n3_gets_used") @@ -190,7 +190,7 @@ def __init__( self.automate_execution = automate_execution for node in nodes: - self.add(node) + self.add_node(node) def _get_linking_channel( self, diff --git a/tests/unit/test_composite.py b/tests/unit/test_composite.py index 1f4faa71d..e5a1e571a 100644 --- a/tests/unit/test_composite.py +++ b/tests/unit/test_composite.py @@ -69,7 +69,7 @@ def test_creator_access_and_registration(self): self.comp.register("demo", "static.demo_nodes") # Test invocation - self.comp.add(self.comp.create.demo.OptionallyAdd(label="by_add")) + self.comp.add_node(self.comp.create.demo.OptionallyAdd(label="by_add")) # Test invocation with attribute assignment self.comp.by_assignment = self.comp.create.demo.OptionallyAdd() node = self.comp.create.demo.OptionallyAdd() @@ -77,7 +77,7 @@ def test_creator_access_and_registration(self): self.assertSetEqual( set(self.comp.nodes.keys()), set(["by_add", "by_assignment"]), - msg=f"Expected one node label generated automatically from the add call " + msg=f"Expected one node label generated automatically from the add_node call " f"and the other from the attribute assignment, but got " f"{self.comp.nodes.keys()}" ) @@ -88,7 +88,7 @@ def test_creator_access_and_registration(self): def test_node_addition(self): # Validate the four ways to add a node - self.comp.add(Composite.create.Function(plus_one, label="foo")) + self.comp.add_node(Composite.create.Function(plus_one, label="foo")) self.comp.baz = self.comp.create.Function(plus_one, label="whatever_baz_gets_used") Composite.create.Function(plus_one, label="qux", parent=self.comp) self.assertListEqual( @@ -171,7 +171,7 @@ def test_label_uniqueness(self): self.comp.strict_naming = True # Validate name preservation for each node addition path with self.assertRaises(AttributeError, msg="We have 'foo' at home"): - self.comp.add(self.comp.create.Function(plus_one, label="foo")) + self.comp.add_node(self.comp.create.Function(plus_one, label="foo")) with self.assertRaises( AttributeError, @@ -203,7 +203,7 @@ def test_label_uniqueness(self): ) self.comp.strict_naming = False - self.comp.add(Composite.create.Function(plus_one, label="foo")) + self.comp.add_node(Composite.create.Function(plus_one, label="foo")) self.assertEqual( 2, len(self.comp), @@ -226,9 +226,9 @@ def test_singular_ownership(self): comp2 = AComposite("two") with self.assertRaises(ValueError, msg="Can't belong to two parents"): - comp2.add(node2) + comp2.add_node(node2) comp1.remove(node2) - comp2.add(node2) + comp2.add_node(node2) self.assertEqual( node2.parent, comp2, diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 7b90fe86d..a410f59e7 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -18,7 +18,7 @@ def add_one(x): def add_three_macro(macro): macro.one = SingleValue(add_one) SingleValue(add_one, macro.one, label="two", parent=macro) - macro.add(SingleValue(add_one, macro.two, label="three")) + macro.add_node(SingleValue(add_one, macro.two, label="three")) # Cover a handful of addition methods, # although these are more thoroughly tested in Workflow tests From 478c60d4930785ae8c7bf92a36fe2dcf33ba623c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 7 Dec 2023 12:04:29 -0800 Subject: [PATCH 07/10] Refactor: rename Composite.remove with remove_node for symmetry --- pyiron_workflow/composite.py | 6 +++--- tests/unit/test_composite.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 7570999b1..e83e3f74b 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -88,7 +88,7 @@ class Composite(Node, ABC): Methods: add_node(node: Node): Add the node instance to this subgraph. - remove(node: Node): Break all connections the node has, remove it from this + remove_node(node: Node): Break all connections the node has, remove_node it from this subgraph, and set its parent to `None`. (de)activate_strict_hints(): Recursively (de)activate strict type hints. replace(owned_node: Node | str, replacement: Node | type[Node]): Replaces an @@ -370,7 +370,7 @@ def _ensure_node_is_not_duplicated(self, node: Node, label: str): ) del self.nodes[node.label] - def remove(self, node: Node | str) -> list[tuple[Channel, Channel]]: + def remove_node(self, node: Node | str) -> list[tuple[Channel, Channel]]: """ Remove a node from the `nodes` collection, disconnecting it and setting its `parent` to None. @@ -443,7 +443,7 @@ def replace(self, owned_node: Node | str, replacement: Node | type[Node]) -> Nod # first guaranteed to be an unconnected orphan, there is not yet any permanent # damage is_starting_node = owned_node in self.starting_nodes - self.remove(owned_node) + self.remove_node(owned_node) replacement.label, owned_node.label = owned_node.label, replacement.label self.add_node(replacement) if is_starting_node: diff --git a/tests/unit/test_composite.py b/tests/unit/test_composite.py index e5a1e571a..b6ec4910a 100644 --- a/tests/unit/test_composite.py +++ b/tests/unit/test_composite.py @@ -138,7 +138,7 @@ def test_node_removal(self): # Connect it inside the composite self.comp.foo.inputs.x = self.comp.owned.outputs.y - disconnected = self.comp.remove(node) + disconnected = self.comp.remove_node(node) self.assertIsNone(node.parent, msg="Removal should de-parent") self.assertFalse(node.connected, msg="Removal should disconnect") self.assertListEqual( @@ -153,7 +153,7 @@ def test_node_removal(self): ) node_owned = self.comp.owned - disconnections = self.comp.remove(node_owned.label) + disconnections = self.comp.remove_node(node_owned.label) self.assertEqual( node_owned.parent, None, @@ -227,7 +227,7 @@ def test_singular_ownership(self): comp2 = AComposite("two") with self.assertRaises(ValueError, msg="Can't belong to two parents"): comp2.add_node(node2) - comp1.remove(node2) + comp1.remove_node(node2) comp2.add_node(node2) self.assertEqual( node2.parent, @@ -347,7 +347,7 @@ def different_output_channel(x: int = 0) -> int: ): self.comp.replace(self.comp.n1, another_node) - another_comp.remove(another_node) + another_comp.remove_node(another_node) another_node.inputs.x = replacement.outputs.y with self.assertRaises( ValueError, From a25e00bd021fab01942e24ba1a1b3c2499e84144 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 7 Dec 2023 12:09:30 -0800 Subject: [PATCH 08/10] Refactor: replace Composite.replace with replace_node for symmetry --- pyiron_workflow/composite.py | 8 ++++---- pyiron_workflow/macro.py | 2 +- pyiron_workflow/node.py | 6 +++--- tests/unit/test_composite.py | 24 ++++++++++++------------ 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index e83e3f74b..9ac16668a 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -91,7 +91,7 @@ class Composite(Node, ABC): remove_node(node: Node): Break all connections the node has, remove_node it from this subgraph, and set its parent to `None`. (de)activate_strict_hints(): Recursively (de)activate strict type hints. - replace(owned_node: Node | str, replacement: Node | type[Node]): Replaces an + replace_node(owned_node: Node | str, replacement: Node | type[Node]): Replaces an owned node with a new node, as long as the new node's IO is commensurate with the node being replaced. register(): A short-cut to registering a new node package with the node creator. @@ -389,7 +389,7 @@ def remove_node(self, node: Node | str) -> list[tuple[Channel, Channel]]: del self.nodes[node.label] return disconnected - def replace(self, owned_node: Node | str, replacement: Node | type[Node]) -> Node: + def replace_node(self, owned_node: Node | str, replacement: Node | type[Node]) -> Node: """ Replaces a node currently owned with a new node instance. The replacement must not belong to any other parent or have any connections. @@ -458,7 +458,7 @@ def replace(self, owned_node: Node | str, replacement: Node | type[Node]) -> Nod except Exception as e: # If IO can't be successfully rebuilt using this node, revert changes and # raise the exception - self.replace(replacement, owned_node) # Guaranteed to work since + self.replace_node(replacement, owned_node) # Guaranteed to work since # replacement in the other direction was already a success raise e @@ -525,7 +525,7 @@ def __setattr__(self, key: str, node: Node): and key in self.nodes.keys() ): # When a class is assigned to an existing node, try a replacement - self.replace(key, node) + self.replace_node(key, node) else: super().__setattr__(key, node) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index adf6a53b4..e99f9c1c3 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -162,7 +162,7 @@ class Macro(Composite): >>> # With the replace method >>> # (replacement target can be specified by label or instance, >>> # the replacing node can be specified by instance or class) - >>> replaced = adds_six_macro.replace(adds_six_macro.one, add_two()) + >>> replaced = adds_six_macro.replace_node(adds_six_macro.one, add_two()) >>> # With the replace_with method >>> adds_six_macro.two.replace_with(add_two()) >>> # And by assignment of a compatible class to an occupied node label diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 1c5fdb2f2..2026c0afd 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -900,7 +900,7 @@ def _copy_values( def replace_with(self, other: Node | type[Node]): """ - If this node has a parent, invokes `self.parent.replace(self, other)` to swap + If this node has a parent, invokes `self.parent.replace_node(self, other)` to swap out this node for the other node in the parent graph. The replacement must have fully compatible IO, i.e. its IO must be a superset of @@ -912,9 +912,9 @@ def replace_with(self, other: Node | type[Node]): other (Node|type[Node]): The replacement. """ if self.parent is not None: - self.parent.replace(self, other) + self.parent.replace_node(self, other) else: - warnings.warn(f"Could not replace {self.label}, as it has no parent.") + warnings.warn(f"Could not replace_node {self.label}, as it has no parent.") def __getstate__(self): state = self.__dict__ diff --git a/tests/unit/test_composite.py b/tests/unit/test_composite.py index b6ec4910a..376d3ca86 100644 --- a/tests/unit/test_composite.py +++ b/tests/unit/test_composite.py @@ -270,7 +270,7 @@ def different_output_channel(x: int = 0) -> int: with self.subTest("Verify success cases"): self.assertEqual(3, self.comp.run().y, msg="Sanity check") - self.comp.replace(n1, replacement) + self.comp.replace_node(n1, replacement) out = self.comp.run(x=0) self.assertEqual( (0+2) + 1 + 1, out.y, msg="Should be able to replace by instance" @@ -278,7 +278,7 @@ def different_output_channel(x: int = 0) -> int: self.assertEqual( 0 - 2, out.n1__minus, msg="Replacement output should also appear" ) - self.comp.replace(replacement, n1) + self.comp.replace_node(replacement, n1) self.assertFalse( replacement.connected, msg="Replaced nodes should be disconnected" ) @@ -286,15 +286,15 @@ def different_output_channel(x: int = 0) -> int: replacement.parent, msg="Replaced nodes should be orphaned" ) - self.comp.replace("n2", replacement) + self.comp.replace_node("n2", replacement) out = self.comp.run(x=0) self.assertEqual( (0 + 1) + 2 + 1, out.y, msg="Should be able to replace by label" ) self.assertEqual(1 - 2, out.n2__minus) - self.comp.replace(replacement, n2) + self.comp.replace_node(replacement, n2) - self.comp.replace(n3, x_plus_minus_z) + self.comp.replace_node(n3, x_plus_minus_z) out = self.comp.run(x=0) self.assertEqual( (0 + 1) + 2 + 1, out.y, msg="Should be able to replace with a class" @@ -306,7 +306,7 @@ def different_output_channel(x: int = 0) -> int: msg="Sanity check -- when replacing with class, a _new_ instance " "should be created" ) - self.comp.replace(self.comp.n3, n3) + self.comp.replace_node(self.comp.n3, n3) self.comp.n1 = x_plus_minus_z self.assertEqual( @@ -315,7 +315,7 @@ def different_output_channel(x: int = 0) -> int: msg="Assigning a new _class_ to an existing node should be a shortcut " "for replacement" ) - self.comp.replace(self.comp.n1, n1) # Return to original state + self.comp.replace_node(self.comp.n1, n1) # Return to original state self.comp.n1 = different_input_channel self.assertEqual( @@ -324,7 +324,7 @@ def different_output_channel(x: int = 0) -> int: msg="Different IO should be compatible as long as what's missing is " "not connected" ) - self.comp.replace(self.comp.n1, n1) + self.comp.replace_node(self.comp.n1, n1) self.comp.n3 = different_output_channel self.assertEqual( @@ -333,7 +333,7 @@ def different_output_channel(x: int = 0) -> int: msg="Different IO should be compatible as long as what's missing is " "not connected" ) - self.comp.replace(self.comp.n3, n3) + self.comp.replace_node(self.comp.n3, n3) with self.subTest("Verify failure cases"): self.assertEqual(3, self.comp.run().y, msg="Sanity check") @@ -345,7 +345,7 @@ def different_output_channel(x: int = 0) -> int: ValueError, msg="Should fail when replacement has a parent" ): - self.comp.replace(self.comp.n1, another_node) + self.comp.replace_node(self.comp.n1, another_node) another_comp.remove_node(another_node) another_node.inputs.x = replacement.outputs.y @@ -353,14 +353,14 @@ def different_output_channel(x: int = 0) -> int: ValueError, msg="Should fail when replacement is connected" ): - self.comp.replace(self.comp.n1, another_node) + self.comp.replace_node(self.comp.n1, another_node) another_node.disconnect() with self.assertRaises( ValueError, msg="Should fail if the node being replaced isn't a child" ): - self.comp.replace(replacement, another_node) + self.comp.replace_node(replacement, another_node) @Composite.wrap_as.single_value_node("y") def wrong_hint(x: float = 0) -> float: From f942bc9fdd13958bb18433ef1f724e779f0ef721 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 7 Dec 2023 12:13:16 -0800 Subject: [PATCH 09/10] Update deepdive notebook --- notebooks/deepdive.ipynb | 98 ++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 40 deletions(-) diff --git a/notebooks/deepdive.ipynb b/notebooks/deepdive.ipynb index f61591d6a..d44ba3984 100644 --- a/notebooks/deepdive.ipynb +++ b/notebooks/deepdive.ipynb @@ -524,9 +524,9 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel ran was not connected to run, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:164: 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:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:164: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -752,13 +752,13 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:164: 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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KSlRfn7+3FQOAADiguMwUllZqYGBAe3YsUOBQED5+flqbm6e3B0TCASm3HMkGAzq5Zdf1tNPPz03VQMAgLjh+D4jNnCfEQAAYs9sv795Ng0AALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArIoojDQ2NionJ0cej0eFhYVqbW2dsf/IyIi2b9+u7OxspaWlac2aNdqzZ09EBQMAgPiS7HTAgQMHVF1drcbGRq1bt04/+tGPtHHjRnV2duoLX/hC2DF33nmnPvzwQ+3evVu///u/r76+Pl24cOEzFw8AAGKfyxhjnAwoKSlRQUGBmpqaJttyc3O1adMmNTQ0TOnf0tKib3zjGzp58qSWLFkSUZGDg4Pyer0KBoNKT0+P6HcAAIDomu33t6PTNKOjo2pvb1d5eXlIe3l5uY4cORJ2zGuvvaaioiJ973vf08qVK3Xttdfqscce0+9+9zsnbw0AAOKUo9M0/f39GhsbU0ZGRkh7RkaGent7w445efKkDh8+LI/Ho1deeUX9/f166KGHdPbs2WmvGxkZGdHIyMjk68HBQSdlAgCAGBLRBawulyvktTFmStuE8fFxuVwu7d+/X8XFxbr99tu1c+dOvfDCC9OujjQ0NMjr9U4eWVlZkZQJAABigKMwsmzZMrnd7imrIH19fVNWSyb4fD6tXLlSXq93si03N1fGGJ06dSrsmNraWgWDwcmjp6fHSZkAACCGOAojqampKiwslN/vD2n3+/0qKysLO2bdunU6c+aMPv7448m29957T0lJSVq1alXYMWlpaUpPTw85AABAfHJ8mqampkY//vGPtWfPHr377rt69NFH1d3draqqKkkXVzXuueeeyf533XWXli5dqvvvv1+dnZ1688039fjjj+sv//IvtWjRorn7JAAAICY5vs9IZWWlBgYGtGPHDgUCAeXn56u5uVnZ2dmSpEAgoO7u7sn+v/d7vye/36+/+qu/UlFRkZYuXao777xTTzzxxNx9CgAAELMc32fEBu4zAgBA7JmX+4wAAADMNcIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMCqZNsFIHaMjRu1dZ1V39Cwli/2qDhnidxJLttlAQBiHGEEs9JyIqD6g50KBIcn23xej+oq8rQh32exMgBArOM0DS6r5URAW/cdCwkiktQbHNbWfcfUciJgqTIAQDwgjGBGY+NG9Qc7ZcL8bKKt/mCnxsbD9QAA4PIII5hRW9fZKSsilzKSAsFhtXWdjV5RAIC4QhjBjPqGpg8ikfQDAODTCCOY0fLFnjntBwDApxFGMKPinCXyeT2abgOvSxd31RTnLIlmWQCAOEIYwYzcSS7VVeRJ0pRAMvG6riKP+40AACJGGMFlbcj3qenuAmV6Q0/FZHo9arq7gPuMAAA+E256hlnZkO/TrXmZ3IEVADDnCCOYNXeSS6VrltouAwAQZzhNAwAArIoojDQ2NionJ0cej0eFhYVqbW2dtu8bb7whl8s15fif//mfiIsGAADxw3EYOXDggKqrq7V9+3Z1dHRo/fr12rhxo7q7u2cc98tf/lKBQGDyuOaaayIuGgAAxA/HYWTnzp3asmWLHnjgAeXm5uqpp55SVlaWmpqaZhy3fPlyZWZmTh5utzviogEAQPxwFEZGR0fV3t6u8vLykPby8nIdOXJkxrFf/vKX5fP5dPPNN+vnP/+580oBAEBccrSbpr+/X2NjY8rIyAhpz8jIUG9vb9gxPp9Pu3btUmFhoUZGRvTP//zPuvnmm/XGG2/oj/7oj8KOGRkZ0cjIyOTrwcFBJ2UCAIAYEtHWXpcr9N4SxpgpbRPWrl2rtWvXTr4uLS1VT0+Pvv/9708bRhoaGlRfXx9JaQAAIMY4Ok2zbNkyud3uKasgfX19U1ZLZnLjjTfq/fffn/bntbW1CgaDk0dPT4+TMgEAQAxxFEZSU1NVWFgov98f0u73+1VWVjbr39PR0SGfb/pbiKelpSk9PT3kAAAA8cnxaZqamhpt3rxZRUVFKi0t1a5du9Td3a2qqipJF1c1Tp8+rb1790qSnnrqKa1evVrXXXedRkdHtW/fPr388st6+eWX5/aTAACAmOQ4jFRWVmpgYEA7duxQIBBQfn6+mpublZ2dLUkKBAIh9xwZHR3VY489ptOnT2vRokW67rrr9G//9m+6/fbb5+5TAACAmOUyxhjbRVzO4OCgvF6vgsEgp2wAAIgRs/3+5tk0AADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKwijAAAAKsIIwAAwCrCCAAAsIowAgAArCKMAAAAqwgjAADAKsIIAACwijACAACsIowAAACrCCMAAMAqwggAALCKMAIAAKyKKIw0NjYqJydHHo9HhYWFam1tndW4t956S8nJybrhhhsieVsAABCHHIeRAwcOqLq6Wtu3b1dHR4fWr1+vjRs3qru7e8ZxwWBQ99xzj26++eaIiwUAAPHHZYwxTgaUlJSooKBATU1Nk225ubnatGmTGhoaph33jW98Q9dcc43cbrdeffVVHT9+fNbvOTg4KK/Xq2AwqPT0dCflAgAAS2b7/e1oZWR0dFTt7e0qLy8PaS8vL9eRI0emHff888/rgw8+UF1d3azeZ2RkRIODgyEHAACIT47CSH9/v8bGxpSRkRHSnpGRod7e3rBj3n//fW3btk379+9XcnLyrN6noaFBXq938sjKynJSJgAAiCERXcDqcrlCXhtjprRJ0tjYmO666y7V19fr2muvnfXvr62tVTAYnDx6enoiKRMAAMSA2S1VfGLZsmVyu91TVkH6+vqmrJZI0tDQkI4ePaqOjg5961vfkiSNj4/LGKPk5GS9/vrruummm6aMS0tLU1pampPSAABAjHK0MpKamqrCwkL5/f6Qdr/fr7Kysin909PT9Ytf/ELHjx+fPKqqqrR27VodP35cJSUln616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", + "image/png": "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GzaMrl9ZWak333xTDQ0NOnbsmPbv36/NmzertLRU8+fPn76fBAAAJJSI7zhbVVWl/v5+7dy5U36/X4WFhWpqatKiRYskSX6/P+yeKbfffruCwaB+97vf6Ze//KUuvPBCXX/99Xr00Uen76cAAAAJx2Xi4JzL4OCgvF6vAoGAMjMznR4HAABMwlRfv/kryAAAwEpECgAAsBKRAgAArESkAAAAKxEpAADASkQKAACwEpECAACsRKQAAAArESkAAMBKRAoAALASkQIAAKxEpAAAACsRKQAAwEpECgAAsBKRAgAArESkAAAAKxEpAADASkQKAACwEpECAACsRKQAAAArESkAAMBKRAoAALASkQIAAKxEpAAAACsRKQAAwEpECgAAsBKRAgAArESkAAAAKxEpAADASkQKAACwEpECAACsRKQAAAArESkAAMBKRAoAALASkQIAAKxEpAAAACsRKQAAwEpECgAAsBKRAgAArESkAAAAKxEpAADASkQKAACwEpECAACsRKQAAAArESkAAMBKRAoAALASkQIAAKxEpAAAACsRKQAAwEpECgAAsBKRAgAArESkAAAAKxEpAADASkQKAACwEpECAACsRKQAAAArESkAAMBKRAoAALASkQIAAKxEpAAAACsRKQAAwEpECgAAsBKRAgAArESkAAAAKxEpAADASkQKAACwEpECAACsFFWk1NfXKz8/Xx6PR8XFxWptbf3O9c+cOaMdO3Zo0aJFSk9P1+LFi/Xiiy9GNTAAAEgOqZFu0NjYqC1btqi+vl7Lly/Xs88+q9WrV6uzs1MXX3zxuNusXbtWp0+f1gsvvKDvf//76u3t1blz56Y8PAAASFwuY4yJZINly5apqKhIDQ0No8uWLl2qNWvWqK6ubsz6zc3NuuWWW3Ts2DFlZWVFNeTg4KC8Xq8CgYAyMzOj+hoAACC2pvr6HdHpnrNnz6q9vV0VFRVhyysqKnTgwIFxt3nnnXdUUlKixx57TAsWLNCSJUv0wAMP6Ouvv454WAAAkDwiOt3T19enUCiknJycsOU5OTnq6ekZd5tjx45p37598ng8euutt9TX16e7775bAwMDE16XcubMGZ05c2b048HBwUjGBAAACSCqC2ddLlfYx8aYMctGDA8Py+Vyac+ePSotLdWNN96oXbt26eWXX57waEpdXZ28Xu/oIy8vL5oxAQBA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Wm9w4kCJZj0AQOwQKUho2RmeaV0PABA7RAoSWml+lnxejyZ6o7FL59/lU5qfFcuxAACTQKQgoaW4XaqpLJCkMaEy8nFNZQH3SwEACxEpSHirCn1qWFekXG/4KZ1cr0cN64q4TwoAWIqbuSEprCr0aWVBLnecBYA4QqQgaaS4XSpbPNfpMQAAk8TpHgAAYCUiBQAAWIlIAQAAViJSAACAlYgUAABgJSIFAABYiUgBAABWIlIAAICViBQAAGCluLjjrDFGkjQ4OOjwJAAAYLJGXrdHXscjFReREgwGJUl5eXkOTwIAACIVDAbl9Xoj3s5los2bGBoeHtapU6eUkZEhlyv8D8INDg4qLy9Px48fV2ZmpkMTJjf2gfPYB87i+Xce+8B54+0DY4yCwaDmz58vtzvyK0zi4kiK2+3WwoULv3OdzMxM/sN0GPvAeewDZ/H8O4994Lxv74NojqCM4MJZAABgJSIFAABYKe4jJT09XTU1NUpPT3d6lKTFPnAe+8BZPP/OYx84byb2QVxcOAsAAJJP3B9JAQAAiYlIAQAAViJSAACAlYgUAABgpbiIlPr6euXn58vj8ai4uFitra3fuf7777+v4uJieTweXXrppfr9738fo0kTVyT74M0339TKlSt10UUXKTMzU2VlZXr33XdjOG3iifR3YMT+/fuVmpqqa665ZmYHTAKR7oMzZ85ox44dWrRokdLT07V48WK9+OKLMZo2MUW6D/bs2aOrr75aF1xwgXw+n+644w719/fHaNrE8sEHH6iyslLz58+Xy+XS22+//f9uMy2vxcZyf/zjH82sWbPM888/bzo7O819991n5syZY/7973+Pu/6xY8fMBRdcYO677z7T2dlpnn/+eTNr1izzxhtvxHjyxBHpPrjvvvvMo48+atra2synn35qtm/fbmbNmmUOHToU48kTQ6TP/4jPP//cXHrppaaiosJcffXVsRk2QUWzD26++WazbNky09LSYrq6usw//vEPs3///hhOnVgi3Qetra3G7XabJ5980hw7dsy0traaK6+80qxZsybGkyeGpqYms2PHDvOnP/3JSDJvvfXWd64/Xa/F1kdKaWmpqa6uDlt2xRVXmG3bto27/q9+9StzxRVXhC276667zLXXXjtjMya6SPfBeAoKCkxtbe10j5YUon3+q6qqzK9//WtTU1NDpExRpPvgL3/5i/F6vaa/vz8W4yWFSPfBb3/7W3PppZeGLXvqqafMwoULZ2zGZDGZSJmu12KrT/ecPXtW7e3tqqioCFteUVGhAwcOjLvNhx9+OGb9n/zkJzp48KD+85//zNisiSqaffBtw8PDCgaDysrKmokRE1q0z/9LL72ko0ePqqamZqZHTHjR7IN33nlHJSUleuyxx7RgwQItWbJEDzzwgL7++utYjJxwotkH5eXlOnHihJqammSM0enTp/XGG2/opptuisXISW+6Xout/gODfX19CoVCysnJCVuek5Ojnp6ecbfp6ekZd/1z586pr69PPp9vxuZNRNHsg297/PHH9eWXX2rt2rUzMWJCi+b5/+yzz7Rt2za1trYqNdXqX/G4EM0+OHbsmPbt2yePx6O33npLfX19uvvuuzUwMMB1KVGIZh+Ul5drz549qqqq0tDQkM6dO6ebb75ZTz/9dCxGTnrT9Vps9ZGUES6XK+xjY8yYZf/f+uMtx+RFug9GvPbaa3r44YfV2Nio7OzsmRov4U32+Q+FQrr11ltVW1urJUuWxGq8pBDJ78Dw8LBcLpf27Nmj0tJS3Xjjjdq1a5defvlljqZMQST7oLOzU5s3b9ZDDz2k9vZ2NTc3q6urS9XV1bEYFZqe12Kr/5k1b948paSkjCnl3t7eMYU2Ijc3d9z1U1NTNXfu3BmbNVFFsw9GNDY2auPGjXr99dd1ww03zOSYCSvS5z8YDOrgwYPq6OjQvffeK+n8C6YxRqmpqXrvvfd0/fXXx2T2RBHN74DP59OCBQvC/kT90qVLZYzRiRMndNlll83ozIkmmn1QV1en5cuX68EHH5QkXXXVVZozZ45WrFihRx55hKPqM2y6XoutPpKSlpam4uJitbS0hC1vaWlReXn5uNuUlZWNWf+9995TSUmJZs2aNWOzJqpo9oF0/gjK7bffrldffZVzwFMQ6fOfmZmpjz76SIcPHx59VFdX6/LLL9fhw4e1bNmyWI2eMKL5HVi+fLlOnTqlL774YnTZp59+KrfbrYULF87ovIkomn3w1Vdfye0Of4lLSUmR9N9/0WPmTNtrcUSX2Tpg5G1nL7zwguns7DRbtmwxc+bMMf/617+MMcZs27bNrF+/fnT9kbc93X///aazs9O88MILvAV5iiLdB6+++qpJTU01zzzzjPH7/aOPzz//3KkfIa5F+vx/G+/umbpI90EwGDQLFy40P/3pT83HH39s3n//fXPZZZeZO++806kfIe5Fug9eeuklk5qaaurr683Ro0fNvn37TElJiSktLXXqR4hrwWDQdHR0mI6ODiPJ7Nq1y3R0dIy+BXymXoutjxRjjHnmmWfMokWLTFpamikqKjLvv//+6Oduu+02c91114Wt/7e//c384Ac/MGlpaeaSSy4xDQ0NMZ448USyD6677jojaczjtttui/3gCSLS34FvIlKmR6T74JNPPjE33HCDmT17tlm4cKHZunWr+eqrr2I8dWKJdB889dRTpqCgwMyePdv4fD7zs5/9zJw4cSLGUyeGv/71r9/5//WZei12GcNxLwAAYB+rr0kBAADJi0gBAABWIlIAAICViBQAAGAlIgUAAFiJSAEAAFYiUgAAgJWIFAAAYCUiBQAAWIlIAQAAViJSAACAlYgUAABgpf8D6HyNwGvqJjMAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -849,10 +849,9 @@ "output_type": "stream", "text": [ "n1 == n1) 0.0 > 0.5 False\n", - "n2 == n2) 0.2 > 0.5 False\n", - "n3 == n3) 0.4 > 0.5 False\n", - "n4 == n4) 0.6 > 0.5 True\n", - "n5 == n5) 0.8 > 0.5 True\n" + "n2 == n2) 0.25 > 0.5 False\n", + "n3 == n3) 0.5 > 0.5 False\n", + "n4 == n4) 0.75 > 0.5 True\n" ] } ], @@ -860,11 +859,9 @@ "n1 = greater_than_half(label=\"n1\")\n", "\n", "wf = Workflow(\"my_wf\", n1) # As args at init\n", - "wf.create.SingleValue(n1.node_function, output_labels=\"p1\", label=\"n2\") \n", - "# ^ Instantiating from the class with a function\n", - "wf.add(greater_than_half(label=\"n3\")) # Instantiating then passing to node adder\n", - "wf.n4 = greater_than_half(label=\"will_get_overwritten_with_n4\") # Set attribute to instance\n", - "greater_than_half(label=\"n5\", parent=wf) # By passing the workflow to the node\n", + "wf.add_node(greater_than_half(label=\"n2\")) # Instantiating then passing to node adder\n", + "wf.n3 = greater_than_half(label=\"will_get_overwritten_with_n3\") # Set attribute to instance\n", + "greater_than_half(label=\"n4\", parent=wf) # By passing the workflow to the node\n", "\n", "for i, (label, node) in enumerate(wf.nodes.items()):\n", " x = i / len(wf)\n", @@ -872,6 +869,14 @@ " print(f\"{label} == {node.label}) {x} > 0.5 {node.single_value}\")" ] }, + { + "cell_type": "markdown", + "id": "77c68bcb-089c-4c92-9897-9a7ab9b087c7", + "metadata": {}, + "source": [ + "Nodes can also be removed or replaced with the corresponding `remove_node` or `replace_node` methods." + ] + }, { "cell_type": "markdown", "id": "dd5768a4-1810-4675-9389-bceb053cddfa", @@ -1345,7 +1350,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 32, @@ -1366,7 +1371,7 @@ "\n", "Currently we have a handfull of pre-build nodes available for import from the `nodes` package. Let's use these to quickly put together a workflow for looking at some MD data.\n", "\n", - "To access prebuilt nodes we can `.create` them. This works both from the workflow class _and_ from a workflow instance. In the latter case, created nodes automatically take the creating workflow instance as their `parent`.\n", + "To access prebuilt nodes we can `.create` them. This works both from the workflow class _and_ from a workflow instance.\n", "\n", "There are a few of nodes that are always available under the `Workflow.create.standard` namespace, otherwise we need to register new node packages. This is done with the `register` method, which takes the domain (namespace/key/attribute/whatever you want to call it) under which you want to register the new nodes, and a string import path to a module that has a list of nodes under the name `nodes`, i.e. the module has the property `nodes: list[pyiron_workflow.nodes.Node]`. (This API is subject to change, as we work to improve usability and bring node packages more and more in line with \"FAIR\" principles.)\n", "\n", @@ -1382,7 +1387,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a704c2ac96114ddf9a3710af377b0f6f", + "model_id": "e46b68d4a1f74b6895b171da7c5144df", "version_major": 2, "version_minor": 0 }, @@ -1401,7 +1406,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 33, @@ -1661,7 +1666,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 34, @@ -1719,7 +1724,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:164: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -3135,7 +3140,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 40, @@ -3178,7 +3183,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel ran was not connected to accumulate_and_run, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:164: UserWarning: The channel ran was not connected to accumulate_and_run, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -3219,15 +3224,15 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel job was not connected to job, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:164: UserWarning: The channel job was not connected to job, andthus could not disconnect from it.\n", " warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel accumulate_and_run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:164: UserWarning: The channel accumulate_and_run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel element was not connected to user_input, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:164: UserWarning: The channel element was not connected to user_input, andthus could not disconnect from it.\n", " warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel structure was not connected to structure1, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:164: UserWarning: The channel structure was not connected to structure1, andthus could not disconnect from it.\n", " warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel energy was not connected to energy1, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:164: UserWarning: The channel energy was not connected to energy1, andthus could not disconnect from it.\n", " warn(\n" ] } @@ -3257,7 +3262,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel ran was not connected to accumulate_and_run, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:164: UserWarning: The channel ran was not connected to accumulate_and_run, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -3287,7 +3292,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel ran was not connected to accumulate_and_run, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:164: UserWarning: The channel ran was not connected to accumulate_and_run, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -3356,7 +3361,7 @@ "output_type": "stream", "text": [ "None 1\n", - " \n" + " \n" ] } ], @@ -3442,7 +3447,7 @@ "output_type": "stream", "text": [ "None 1\n", - " \n", + " \n", "Finally 5\n", "b (AddNode) output single-value: 6\n" ] @@ -3455,7 +3460,7 @@ " wf.a2 = add_node(2, 3)\n", " wf.b = add_node(wf.a1, wf.a2)\n", "\n", - " wf.a2.executor = wf.create.Executor()\n", + " wf.a2.executor = executor\n", " wf()\n", " \n", " print(wf.a1.future, wf.a1.outputs.sum.value)\n", @@ -3504,7 +3509,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "6.010571043996606\n" + "6.014589287980925\n" ] } ], @@ -3536,7 +3541,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "3.023019565967843\n" + "2.5630508870235644\n" ] } ], @@ -3669,9 +3674,9 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to true, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:164: UserWarning: The channel run was not connected to true, andthus could not disconnect from it.\n", " warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:159: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:164: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", " warn(\n" ] } @@ -3752,11 +3757,24 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.991 > 0.2\n", - "0.325 > 0.2\n", - "0.891 > 0.2\n", - "0.055 <= 0.2\n", - "Finally 0.055\n" + "0.337 > 0.2\n", + "0.563 > 0.2\n", + "0.966 > 0.2\n", + "0.979 > 0.2\n", + "0.327 > 0.2\n", + "0.581 > 0.2\n", + "0.626 > 0.2\n", + "0.739 > 0.2\n", + "0.828 > 0.2\n", + "0.769 > 0.2\n", + "0.462 > 0.2\n", + "0.973 > 0.2\n", + "0.992 > 0.2\n", + "0.801 > 0.2\n", + "0.439 > 0.2\n", + "0.808 > 0.2\n", + "0.045 <= 0.2\n", + "Finally 0.045\n" ] } ], From eea086dfaa0fea94095fcc450c579543c51b3638 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Thu, 7 Dec 2023 20:29:35 +0000 Subject: [PATCH 10/10] Format black --- pyiron_workflow/composite.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 9ac16668a..f0ef895b8 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -389,7 +389,9 @@ def remove_node(self, node: Node | str) -> list[tuple[Channel, Channel]]: del self.nodes[node.label] return disconnected - def replace_node(self, owned_node: Node | str, replacement: Node | type[Node]) -> Node: + def replace_node( + self, owned_node: Node | str, replacement: Node | type[Node] + ) -> Node: """ Replaces a node currently owned with a new node instance. The replacement must not belong to any other parent or have any connections.