-
Notifications
You must be signed in to change notification settings - Fork 3
Description
But remain output channels.
E.g., something like the following should work:
from pyiron_workflow import Workflow
class Foo:
bar = {"some_list": [1, 2, 3, 4]}
wf = Workflow("output_stuff")
wf.start = wf.create.standard.UserInput(Foo())
wf.attribute_access = wf.create.standard.UserInput(
wf.start.outputs.user_input.bar
)
# Asking for a non-existent attribute on an output channel
# should give a new channel with that delayed operation
wf.item_access = wf.create.standard.UserInput(
wf.attribute_access["some_list"]
)
# In this case, UserInput is a SingleValue node
# so we can pull the same trick at the node-level
wf.slice_access = wf.create.standard.UserInput(
wf.item_access[2:]
)
# And, of course, slices as a subset of item access
# should work fine
wf()
>>> {"slice_access__user_input": [3, 4]}
A similar trick should be possible with other basic operations, e.g.
from pyiron_workflow import Workflow
wf = Workflow("output_stuff")
wf.part_a = wf.create.standard.UserInput([1, 2])
wf.part_b = wf.create.standard.UserInput([3, 4])
wf.part_c = wf.create.standard.UserInput([5, 6])
wf.sum = wf.create.standard.UserInput(
wf.part_a + wf.part_b + wf.part_c
)
wf()
>>> {"sum__user_input": [1, 2, 3, 4, 5, 6]}
The catch is how to do this in a delayed way that is fully compliant with the rest of the node architecture. The solution I came up with is to modify the __getattr__
and __getitem__
of OutputData
such that they fall back on creating an entire new node instance (with the same parentage as the channel's node), defined in the standard package, e.g. something like
@single_value_node("item")
def GetItem(data, item):
return data[item]
Such that
wf.item_access = wf.create.standard.UserInput(
wf.attribute_access["some_list"]
)
is equivalent to
get_some_list = wf.create.standard.GetItem(wf.attribute_access, "some_list")
wf.item_access = wf.create.standard.UserInput(get_some_list)
With similar nodes defined in the standard package for adding, multiplying, etc., and a similar overload of the corresponding dunder methods (__add__
etc.)
And, of course, since the dynamically returned nodes are also just nodes, it's not a problem to chain these, as in the addition example above or things like wf.some_node.outputs.my_numpy_array.T[:5]
Pros:
- More intuitive and compact right-hand-sides for writing workflows
- Fully delayed, fully compliant with the graph philosophy, and shows up in the graph drawing
- Like other nodes, can define these as temporary variables and connect them to multiple different inputs
Cons:
- Is implicitly adding nodes to the graph
- Creates an asymmetry between LHS and RHS
- I.e.
wf.some_node.inputs.some_array[5:7] = wf.length_two_list_svn
doesn't work, whilewf.length_two.inputs.list = wf.some_array_node[5:7]
does - I am open to help here, but as far as I can tell this is a fundamental difference between appending new nodes downstream in a data flow and inserting new nodes upstream in a data flow, the latter of which does not work reliably in a world where we may allow multiple connections to an input data channel.
- I.e.
- Does not play very nicely with type hinting, leading to un-hinted nodes
- Runs the chance of failing late because it's delayed, e.g. if you wind up asking for at item/attribute that isn't there, or adding/multiplying/subtracting two types that don't support this...you don't find out until you hit a runtime failure
For type hinting, I can already see a technical solution, but it's verbose for at least one of the user or developer. On the user side, we can always add a type hint post-facto:
wf.my_list = wf.create.doesnotexist.MyTypeHintedListOfInts([1, 2, 3])
wf.last_item = wf.my_list[-1]
wf.last_item.outputs.item.type_hint = int
But I think it should also be possible to push this work onto the developer, by having them overload the dunders, like:
class MyTypeHintedListOfInts(Node):
...
def __getitem__(self, item):
item_access_node = super().__getitem__(item)
if isinstance(item, int):
item_access_node.outputs.item.type_hint = int
elif some_function_to_check_if_its_a_slice(item):
item_access_node.outputs.item.type_hint = list[int]
# Or whatever to process list[int] as an assignable type hint
This is ugly, but I'm happy that it's at least possible! Since type hinting is anyhow an optional feature, I don't think the negative impact on type hints should stop these changes from going forward.
Overall I'm super excited about this idea, and at least on the output side it resolves a concern that came up way back when we showed ironflow to the MPCDF guys about how these slicing/indexing/transposing nodes are always popping up when one wants to do node based workflows. Of course, under the hood we do still have such a node, but it is beautifully (dangerously 😝) hidden from the user -- at least for text users, for GUI users more thought is still required.
EDIT: fix example typo