Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#140 #141

Closed
wants to merge 8 commits into from
Closed

#140 #141

wants to merge 8 commits into from

Conversation

Onandon11
Copy link
Collaborator

I tried to clean-up the code, but the creation of the iterables array is quite low level. Also in combination with subinputs containing IterationPlug or nested IterationPlugs.

Please feel free to comment on the code :)

@Onandon11 Onandon11 linked an issue Sep 11, 2020 that may be closed by this pull request
@Onandon11 Onandon11 self-assigned this Sep 11, 2020
@neuneck
Copy link
Collaborator

neuneck commented Sep 24, 2020

On first glance, I wonder whether it wouldn't be cleaner and easier to create IterationNode as a subclass of INode. That way, you can forego all the checks and discard -1 as a sentinel value for no iteration. If it's supposed to iterate, it's gonna be an iteration node.

Another note: Personally, I'm not a big fan of assert in python - the exception system is a great tool, if the exceptions are specific enough and the proper exceptions are raised. As an example, when you check the iteration_count variable, I find it preferable to use something like

from numbers import Integral
if iteration_count is None:
    pass
elif not isinstance(iteration_count, Integral):
    raise TypeError("iteration_count has to be an integer")
elif 0 < iteration_count:
    raise ValueError("iteration_count has to be positive")

This way, the errors raised are more informative.

@Onandon11
Copy link
Collaborator Author

Onandon11 commented Sep 24, 2020

I thought about making the IterationNode a child of the INode but in that way you need to somehow specify it when creating the node. For example I mainly use the following syntax

@Node(outputs=['out'])  # How to specify that out is an `IterationNode`? 
def foo(baz)
    return {'out': baz}

Also the IterationPlug is nothing more than a list with a specific type, just to trigger the iteration within the evaluation of the Node.

I agree on the assert I use them quite a lot for type checking, but it isn't the best way.

@neuneck
Copy link
Collaborator

neuneck commented Sep 24, 2020

I see two options here: Either the @Node decorator takes an optional argument iteration=False/True to know whether to create a basic INode or an IterationNode, or we set up an @IterNode decorator that creates iteration nodes. I prefer the second option, since it's more explicit.

If the existence of a specific IterationNode class makes the IterationPlug obsolete, that's all the more reason to build it, imho.

@neuneck
Copy link
Collaborator

neuneck commented Sep 24, 2020

To share this a tad more explicitly, because it caused me pain as a user in the past: The recommended way to check for integers is to

from numbers import Integral
if isinstance(unkown_number, Integral):
    do_stuff_with_integer(unknown_number)

The abstact class Integral does a number of checks for whether a value is an integer. This way, it will e.g. also accept numpy's various integer types, which don't inherit from the built-in int.

@Onandon11
Copy link
Collaborator Author

I see two options here: Either the @Node decorator takes an optional argument iteration=False/True to know whether to create a basic INode or an IterationNode, or we set up an @IterNode decorator that creates iteration nodes. I prefer the second option, since it's more explicit.

Agree but what about Nodes that Iterate depending on their input? I can imagine a case where a Node will output an IterationPlug depending on it's input. So at compile time it's not always clear if an iteration will happen.

@neuneck
Copy link
Collaborator

neuneck commented Sep 28, 2020

But won't you have to set up an IterationPlug at "compile time" to iterate anyways? If I understand the current implementation correctly, anything that's not in an IterationPlug will not get iterated over, and you'll have to define the plugs when setting up the graph anyways.

@Onandon11
Copy link
Collaborator Author

I understand the miscommunication, it's because I called it an ItertationPlug, which is wrong... The output is just a normal OutputPlug but the value of the OutputPlug (eg. plug.value = ItertationPlug([1, 2, 3])).

The type of the value is just a trigger for subsequent nodes to iterate over and recall itself. Naming it a plug wasn't correct maybe..

Copy link
Collaborator

@neuneck neuneck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still feel that IterationNode should be a subtype of INode. They add a lot of complexity, which will make it harder to debug/extend INodes in the future. If you need all your nodes to have these capabilities, then it should be easy to build your entire graph from them.

flowpipe/node.py Outdated
outputs = {}
for arguments in zip(*iterables.values()): # Loop over the list of kwargs in iterables
[inputs.update(arg) for arg in arguments] # Update the inputs
output = self._evaluate(**inputs) # Compute the node
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we absolutely need to do a recursion here? It's a dangerous pattern, because the control flow becomes dynamic and therefore hard to understand / debug.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imho I think we do.. because what are we going to do when a Node gets an Iterable and subinputs with Iterables, then it need to recurse..

But I agree on the ability to debug the code, although I think the main problem is to debug the flowpipe code itself. Not the implementation of the self.compute function.

flowpipe/node.py Outdated Show resolved Hide resolved
flowpipe/node.py Outdated Show resolved Hide resolved
@Onandon11
Copy link
Collaborator Author

Okay, then how does a node know whether to set the output as a regular list or as a list to iterate over? As far as I can tell, this is decided by the iteration_count attribute of a node, which is set when initializing a node. Or is there some other mechanism to trigger the iteration?

Beforehand it doesn't knows.. if you make a Node like this:

@Node(outputs=['out'])
def foo(baz):
    return {'out': baz}

This Node is not specifically made for iteration, but when baz is an IterationPlug (or how we would like to call it), it will recall itself for every value in baz and create an output out which is then becomes also an IterationPlug

@neuneck
Copy link
Collaborator

neuneck commented Oct 2, 2020

I understand. But what would stop you from creating all nodes should potentially iterate over inputs as IternationNode? In case they don't receive an IterationPlug typed input, their behaviour is the same as a regular node.

I feel uncomfortable if the behaviour of my nodes is defined at run-time based on (potentially dynamic) inputs. That's why I would feel more at ease if this feature was part of a separate node type. Flowpipe's graph executing sucessfully right now has a pretty solid guarantee that each (dirty) node gets executed exactly once, which is a very powerful statement for such a modular system.

flowpipe/node.py Outdated Show resolved Hide resolved
flowpipe/plug.py Outdated Show resolved Hide resolved
flowpipe/node.py Outdated Show resolved Hide resolved
@PaulSchweizer
Copy link
Owner

Sorry for joining the discussion so late.

I agree with @neuneck's requests and the reasons given. To sum it up:

  • The IterationNode should be a suclass of INode.
  • It will be available via it's own decorator @IteratorNode.

@Onandon11 would these changes interfere in any way with your usage of flowpipe?

@Onandon11
Copy link
Collaborator Author

Alright, we will manage :) I cannot oversee the impact on our repo currently, but we will find out fast enough.

I will try to implement it this week :)

@Onandon11
Copy link
Collaborator Author

I was starting to implement the proposal. But it's still a bit unclear for me, do you want and IterationNode which is a subclass of INode and will always iterate over all of it's inputs?

@neuneck
Copy link
Collaborator

neuneck commented Oct 23, 2020

Basically yes, but a mix of iterative and fixed inputs makes a lot of sense, IMHO. My suggested solution would be to have an IterationPlug, a subclass of InputPlug, which will lead to the node iterating over values inside that plug. This makes very clear what will happen at runtime, just form the way nodes are set up. The output can then just be set to a list of the results of the compute() method. If the next node should iterate over those outputs, all you'd have to do is connect the output up to an IterationPlug.

There are some further things you should decide on:

  • If there are multiple iteration inputs, should they be ziped?
  • If so, what should happen when the inputs have a different length?
  • Should the compute() be run on the cartesian product of the input iterables?
  • Should that be something the user defines, and if so, how?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Iteration node
3 participants