Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
59124c4
Make a decorator for handling status changes
liamhuber Oct 11, 2023
83d491d
Apply the new decorator to the run function
liamhuber Oct 11, 2023
1958b6b
Inline the length check on function output
liamhuber Oct 11, 2023
37de036
Allow `process_run_result` to actually modify the return value
liamhuber Oct 11, 2023
ca0ca51
Have input channels pull instead of output channels pushing
liamhuber Oct 11, 2023
d297535
Update the notebook
liamhuber Oct 11, 2023
8811385
Merge pull request #24 from pyiron/data_channels_pull
liamhuber Oct 11, 2023
0094e7d
Rename input channel pull to fetch
liamhuber Oct 12, 2023
ee4fd8a
Create shortcuts to fetch input on IO and Node
liamhuber Oct 12, 2023
c22717d
Make the finishing method a run variable
liamhuber Oct 12, 2023
1d7b5c8
Split finishing and emitting `ran` and hint usage in `pull`
liamhuber Oct 12, 2023
9641bc5
Update docstrings
liamhuber Oct 12, 2023
7c36560
Always follow the pattern of passing args to on_run
liamhuber Oct 12, 2023
62b987e
Add a method to force the node operation locally with whatever data y…
liamhuber Oct 12, 2023
218ad91
Add run docstring
liamhuber Oct 12, 2023
5ef80f4
Format black
pyiron-runner Oct 12, 2023
cbf16ee
Refactor: slide
liamhuber Oct 12, 2023
328b956
Provide a single interface for updating node input
liamhuber Oct 12, 2023
1b90d78
Add to docstring
liamhuber Oct 12, 2023
d9aa842
Merge pull request #25 from pyiron/better_input_interface
liamhuber Oct 12, 2023
7769270
Return disconnections when removing nodes
liamhuber Oct 12, 2023
3b3cfdb
:bug: make removing by label do the same thing as by object
liamhuber Oct 12, 2023
c40a4b1
Add a method for nodes to copy another node's connections
liamhuber Oct 12, 2023
ee4054c
Match panel keys, not channel labels
liamhuber Oct 12, 2023
ef58dc4
Allow composite children to be replaced by another node
liamhuber Oct 12, 2023
7b05afe
Allow replacing with a compatible class instead of an instance
liamhuber Oct 13, 2023
c011e5b
Refactor tests
liamhuber Oct 13, 2023
93728c9
Update docstring
liamhuber Oct 13, 2023
d24d951
Format black
pyiron-runner Oct 13, 2023
be32f0f
Add syntactic sugar for triggering replacement from the replacee
liamhuber Oct 13, 2023
e3c13bc
Merge remote-tracking branch 'origin/allow_replacing_nodes' into allo…
liamhuber Oct 13, 2023
346bcab
Add syntactic sugar for replacing via class assignment
liamhuber Oct 13, 2023
3ca64bd
:bug: fix docstring example typo
liamhuber Oct 13, 2023
e540a78
:bug: remove removed nodes from starting_nodes if they're there
liamhuber Oct 13, 2023
7606ab0
If you're replacing a starting node, make the replacement a starter too
liamhuber Oct 13, 2023
bb69bc3
Make sure Macros update their IO on replacement
liamhuber Oct 13, 2023
d7f280c
Update macro docstring
liamhuber Oct 13, 2023
de0699a
Update the demo notebook
liamhuber Oct 13, 2023
51f2750
Merge pull request #26 from pyiron/allow_replacing_nodes
liamhuber Oct 13, 2023
43f37b5
Allow the function node to be a method
liamhuber Oct 13, 2023
fd2a290
Merge pull request #27 from pyiron/make_node_function_available
liamhuber Oct 13, 2023
c3d24a0
Execute demo notebook
liamhuber Oct 16, 2023
bfb5359
Merge branch 'main' into refactor_run_cycle
liamhuber Oct 16, 2023
31c661c
Allow connection copying to be more relaxed and pass over failures
liamhuber Oct 16, 2023
43d78a3
Be strict about bad connections
liamhuber Oct 16, 2023
a626eed
Test that connection copying is handling type hints well
liamhuber Oct 16, 2023
ad3e446
Add method for copying data values
liamhuber Oct 16, 2023
56c7b6a
Put copying right on the channels
liamhuber Oct 17, 2023
2acb763
Introduce a custom exception for invalid connections and refactor
liamhuber Oct 17, 2023
eecd699
Refactor and update docs
liamhuber Oct 17, 2023
28f89f0
Use the new custom error in function tests
liamhuber Oct 17, 2023
8f02e72
Use new method in copying values
liamhuber Oct 17, 2023
009cbad
Add return values for reversion and update docs
liamhuber Oct 17, 2023
e82ed4c
:bug: pass the right object
liamhuber Oct 17, 2023
8ee7182
Add tests for value copying
liamhuber Oct 17, 2023
4a5bb94
Provide a public interface that copies both at once
liamhuber Oct 17, 2023
82c38f2
Use the new public interface when composite is replacing a node
liamhuber Oct 17, 2023
ce3ba33
Rerun the demo notebook
liamhuber Oct 17, 2023
5a13ee2
Format black
pyiron-runner Oct 17, 2023
3e4da9e
Merge pull request #34 from pyiron/copy_more_than_connections
liamhuber Oct 18, 2023
a07eb78
Introduce coupling between input channels
liamhuber Oct 18, 2023
cf81bbd
Format black
pyiron-runner Oct 18, 2023
e120a57
Improve docstring and type hinting
liamhuber Oct 18, 2023
2493495
Refactor: change signature
liamhuber Oct 18, 2023
9a59975
Refactor: rename variable
liamhuber Oct 18, 2023
20f5ce2
Break out a line of the IO creation into its own method
liamhuber Oct 18, 2023
36b70bb
:bug: apply the channel getter to _both_ mapped and unmapped cases
liamhuber Oct 18, 2023
c3ef618
:bug: actually use the key you try-excepted to get
liamhuber Oct 18, 2023
4d6ae7e
Revert change to io panel creation
liamhuber Oct 18, 2023
b1db7f7
Revert change to io panel creation
liamhuber Oct 18, 2023
2f33746
Build IO by _value_ in macro
liamhuber Oct 18, 2023
ca37c30
Update macro tests
liamhuber Oct 18, 2023
402ea7d
Move the value receiver up to DataChannel
liamhuber Oct 18, 2023
604fc56
Always keep macro output values synchronized
liamhuber Oct 18, 2023
08c74cc
Rebuild connections when you rebuild IO
liamhuber Oct 18, 2023
9146b00
Reexecute demo notebook
liamhuber Oct 18, 2023
80c5e5a
Hint how to run with executor
liamhuber Oct 18, 2023
92b00a9
Swap labels and return the replaced node for easier reversion
liamhuber Oct 19, 2023
2753081
Revert IO reconstruction if it fails
liamhuber Oct 19, 2023
3fe7da0
Revert replacement in macros if IO construction fails
liamhuber Oct 19, 2023
84496d5
Commit the tests used to actually work out the last three commits
liamhuber Oct 19, 2023
1d57fc3
Format black
pyiron-runner Oct 19, 2023
01525b8
Fix type hint typo
liamhuber Oct 19, 2023
ca97ad2
Make executor a bool
liamhuber Oct 19, 2023
d6df8b7
Refactor composite's run processing
liamhuber Oct 19, 2023
31deb97
Allow composite to update its executor value
liamhuber Oct 19, 2023
8a83feb
Write macro tests to define expected behaviour
liamhuber Oct 19, 2023
5796227
Mess with __getstate__ and __setstate__ until they work
liamhuber Oct 19, 2023
f119489
:bug: remove debug print
liamhuber Oct 19, 2023
b1a5551
:bug: switch IsNot to Is then comment out the test
liamhuber Oct 19, 2023
7f2d74b
Test that the workflow will work with an executor as well
liamhuber Oct 19, 2023
ba4030a
Tidy imports
liamhuber Oct 19, 2023
5debae1
Update node docstring
liamhuber Oct 19, 2023
99806cc
Fail hard if a function node tries to use self with an executor
liamhuber Oct 19, 2023
bbab1cf
Add back the warning about executors and self
liamhuber Oct 19, 2023
c6ee4ac
Format black
pyiron-runner Oct 19, 2023
7cc3e19
Implement __getstate__ for compatibility with python <3.11
liamhuber Oct 19, 2023
21459a5
Merge remote-tracking branch 'origin/executors_for_composite' into ex…
liamhuber Oct 19, 2023
ba9a9a2
Disable node package registration and simplify atomistics and standard
liamhuber Oct 20, 2023
13f3d29
Things seem fine but add a note
liamhuber Oct 20, 2023
b9a9e7e
Format black
pyiron-runner Oct 20, 2023
8f84103
Merge pull request #39 from pyiron/executors_for_composite
liamhuber Oct 20, 2023
857eb94
Manually set __get/setstate__ in DotDict for python <3.11
liamhuber Oct 22, 2023
69d15c3
:bug: fig typo in method name
liamhuber Oct 22, 2023
a64dc95
Exhaustively set __get/setstate__ everywhere we override __getattr__
liamhuber Oct 22, 2023
5d0f39b
:bug: have __setstate__ play nicely with __setattr__
liamhuber Oct 22, 2023
4809a6e
Merge pull request #37 from pyiron/no_io_by_reference_for_macros
liamhuber Oct 22, 2023
9e7c288
Merge pull request #36 from pyiron/data_links
liamhuber Oct 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,435 changes: 1,367 additions & 1,068 deletions notebooks/workflow_example.ipynb

Large diffs are not rendered by default.

319 changes: 182 additions & 137 deletions pyiron_workflow/channels.py

Large diffs are not rendered by default.

217 changes: 182 additions & 35 deletions pyiron_workflow/composite.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from __future__ import annotations

from abc import ABC
from abc import ABC, abstractmethod
from functools import partial
from typing import Literal, Optional, TYPE_CHECKING

Expand All @@ -19,7 +19,7 @@
from pyiron_workflow.util import logger, DotDict, SeabornColors

if TYPE_CHECKING:
from pyiron_workflow.channels import Channel
from pyiron_workflow.channels import Channel, InputData, OutputData


class Composite(Node, ABC):
Expand Down Expand Up @@ -106,7 +106,7 @@ def __init__(
self._outputs_map = None
self.inputs_map = inputs_map
self.outputs_map = outputs_map
self.nodes: DotDict[str:Node] = DotDict()
self.nodes: DotDict[str, Node] = DotDict()
self.starting_nodes: list[Node] = []
self._creator = self.create
self.create = self._owned_creator # Override the create method from the class
Expand Down Expand Up @@ -138,17 +138,6 @@ def _owned_creator(self):
"""
return OwnedCreator(self, self._creator)

@property
def executor(self) -> None:
return None

@executor.setter
def executor(self, new_executor):
if new_executor is not None:
raise NotImplementedError(
"Running composite nodes with an executor is not yet supported"
)

def to_dict(self):
return {
"label": self.label,
Expand All @@ -160,11 +149,31 @@ def on_run(self):
return self.run_graph

@staticmethod
def run_graph(self):
for node in self.starting_nodes:
def run_graph(_nodes: dict[Node], _starting_nodes: list[Node]):
for node in _starting_nodes:
node.run()
return _nodes

@property
def run_args(self) -> dict:
return {"_nodes": self.nodes, "_starting_nodes": self.starting_nodes}

def process_run_result(self, run_output):
if run_output is not self.nodes:
# Then we probably ran on a parallel process and have an unpacked future
self._update_children(run_output)
return DotDict(self.outputs.to_value_dict())

def _update_children(self, children_from_another_process: DotDict[str, Node]):
"""
If you receive a new dictionary of children, e.g. from unpacking a futures
object of your own children you sent off to another process for computation,
replace your own nodes with them, and set yourself as their parent.
"""
for child in children_from_another_process.values():
child.parent = self
self.nodes = children_from_another_process

def disconnect_run(self) -> list[tuple[Channel, Channel]]:
"""
Disconnect all `signals.input.run` connections on all child nodes.
Expand Down Expand Up @@ -249,35 +258,78 @@ def get_data_digraph(self) -> dict[str, set[str]]:

return digraph

@property
def run_args(self) -> dict:
return {"self": self}

def _build_io(
self,
io: Inputs | Outputs,
target: Literal["inputs", "outputs"],
key_map: dict[str, str] | None,
i_or_o: Literal["inputs", "outputs"],
key_map: dict[str, str | None] | None,
) -> Inputs | Outputs:
"""
Build an IO panel for exposing child node IO to the outside world at the level
of the composite node's IO.

Args:
target [Literal["inputs", "outputs"]]: Whether this is I or O.
key_map [dict[str, str]|None]: A map between the default convention for
mapping child IO to composite IO (`"{node.label}__{channel.label}"`) and
whatever label you actually want to expose to the composite user. Also
allows non-standards channel exposure, i.e. exposing
internally-connected channels (which would not normally be exposed) by
providing a string-to-string map, or suppressing unconnected channels
(which normally would be exposed) by providing a string-None map.

Returns:
(Inputs|Outputs): The populated panel.
"""
key_map = {} if key_map is None else key_map
io = Inputs() if i_or_o == "inputs" else Outputs()
for node in self.nodes.values():
panel = getattr(node, target)
panel = getattr(node, i_or_o)
for channel_label in panel.labels:
channel = panel[channel_label]
default_key = f"{node.label}__{channel_label}"
try:
if key_map[default_key] is not None:
io[key_map[default_key]] = channel
io_panel_key = key_map[default_key]
if io_panel_key is not None:
io[io_panel_key] = self._get_linking_channel(
channel, io_panel_key
)
except KeyError:
if not channel.connected:
io[default_key] = channel
io[default_key] = self._get_linking_channel(
channel, default_key
)
return io

@abstractmethod
def _get_linking_channel(
self,
child_reference_channel: InputData | OutputData,
composite_io_key: str,
) -> InputData | OutputData:
"""
Returns the channel that will be the link between the provided child channel,
and the composite's IO at the given key.

The returned channel should be fully compatible with the provided child channel,
i.e. same type, same type hint... (For instance, the child channel itself is a
valid return, which would create a composite IO panel that works by reference.)

Args:
child_reference_channel (InputData | OutputData): The child channel
composite_io_key (str): The key under which this channel will be stored on
the composite's IO.

Returns:
(Channel): A channel with the same type, type hint, etc. as the reference
channel passed in.
"""
pass

def _build_inputs(self) -> Inputs:
return self._build_io(Inputs(), "inputs", self.inputs_map)
return self._build_io("inputs", self.inputs_map)

def _build_outputs(self) -> Outputs:
return self._build_io(Outputs(), "outputs", self.outputs_map)
return self._build_io("outputs", self.outputs_map)

def add(self, node: Node, label: Optional[str] = None) -> None:
"""
Expand Down Expand Up @@ -353,17 +405,96 @@ def _ensure_node_is_not_duplicated(self, node: Node, label: str):
)
del self.nodes[node.label]

def remove(self, node: Node | str):
if isinstance(node, Node):
node.parent = None
node.disconnect()
del self.nodes[node.label]
def remove(self, node: Node | str) -> list[tuple[Channel, Channel]]:
"""
Remove a node from the `nodes` collection, disconnecting it and setting its
`parent` to None.

Args:
node (Node|str): The node (or its label) to remove.

Returns:
(list[tuple[Channel, Channel]]): Any connections that node had.
"""
node = self.nodes[node] if isinstance(node, str) else node
node.parent = None
disconnected = node.disconnect()
if node in self.starting_nodes:
self.starting_nodes.remove(node)
del self.nodes[node.label]
return disconnected

def replace(self, owned_node: Node | str, replacement: Node | type[Node]) -> Node:
"""
Replaces a node currently owned with a new node instance.
The replacement must not belong to any other parent or have any connections.
The IO of the new node must be a perfect superset of the replaced node, i.e.
channel labels need to match precisely, but additional channels may be present.
After replacement, the new node will have the old node's connections, label,
and belong to this composite.
The labels are swapped, such that the replaced node gets the name of its
replacement (which might be silly, but is useful in case you want to revert the
change and swap the replaced node back in!)

If replacement fails for some reason, the replacement and replacing node are
both returned to their original state, and the composite is left unchanged.

Args:
owned_node (Node|str): The node to replace or its label.
replacement (Node | type[Node]): The node or class to replace it with. (If
a class is passed, it has all the same requirements on IO compatibility
and simply gets instantiated.)

Returns:
(Node): The node that got removed
"""
if isinstance(owned_node, str):
owned_node = self.nodes[owned_node]

if owned_node.parent is not self:
raise ValueError(
f"The node being replaced should be a child of this composite, but "
f"another parent was found: {owned_node.parent}"
)

if isinstance(replacement, Node):
if replacement.parent is not None:
raise ValueError(
f"Replacement node must have no parent, but got "
f"{replacement.parent}"
)
if replacement.connected:
raise ValueError("Replacement node must not have any connections")
elif issubclass(replacement, Node):
replacement = replacement(label=owned_node.label)
else:
del self.nodes[node]
raise TypeError(
f"Expected replacement node to be a node instance or node subclass, but "
f"got {replacement}"
)

replacement.copy_io(owned_node) # If the replacement is incompatible, we'll
# fail here before we've changed the parent at all. Since the replacement was
# first guaranteed to be an unconnected orphan, there is not yet any permanent
# damage
is_starting_node = owned_node in self.starting_nodes
self.remove(owned_node)
replacement.label, owned_node.label = owned_node.label, replacement.label
self.add(replacement)
if is_starting_node:
self.starting_nodes.append(replacement)
return owned_node

def __setattr__(self, key: str, node: Node):
if isinstance(node, Node) and key != "parent":
self.add(node, label=key)
elif (
isinstance(node, type)
and issubclass(node, Node)
and key in self.nodes.keys()
):
# When a class is assigned to an existing node, try a replacement
self.replace(key, node)
else:
super().__setattr__(key, node)

Expand Down Expand Up @@ -427,6 +558,16 @@ def __getattr__(self, item):

return value

def __getstate__(self):
# Compatibility with python <3.11
return self.__dict__

def __setstate__(self, state):
# Because we override getattr, we need to use __dict__ assignment directly in
# __setstate__
self.__dict__["_parent"] = state["_parent"]
self.__dict__["_creator"] = state["_creator"]


class OwnedNodePackage:
"""
Expand All @@ -443,3 +584,9 @@ def __getattr__(self, item):
if issubclass(value, Node):
value = partial(value, parent=self._parent)
return value

def __getstate__(self):
return self.__dict__

def __setstate__(self, state):
self.__dict__ = state
Loading