Skip to content

Commit

Permalink
Add if, elseif, and else.
Browse files Browse the repository at this point in the history
  • Loading branch information
syamajala committed Sep 26, 2018
1 parent 3d8b6dd commit e76a39d
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 4 deletions.
1 change: 1 addition & 0 deletions graphkit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
# For backwards compatibility
from .base import Operation
from .network import Network
from .control import If, ElseIf, Else
21 changes: 21 additions & 0 deletions graphkit/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def __init__(self, **kwargs):
self.provides = kwargs.get('provides')
self.params = kwargs.get('params', {})
self.color = kwargs.get('color', None)
self.order = 0

# call _after_init as final step of initialization
self._after_init()
Expand Down Expand Up @@ -165,3 +166,23 @@ def __getstate__(self):
state = Operation.__getstate__(self)
state['net'] = self.__dict__['net']
return state


class Control(Operation):

def __init__(self, **kwargs):
super(Control, self).__init__(**kwargs)

def __repr__(self):
"""
Display more informative names for the Operation class
"""
if hasattr(self, 'condition_needs'):
return u"%s(name='%s', needs=%s, provides=%s, condition_needs=%s)" % \
(self.__class__.__name__,
self.name,
self.needs,
self.provides,
self.condition_needs)
else:
return super(Control, self).__repr__()
47 changes: 47 additions & 0 deletions graphkit/control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
This sub-module contains statements that can be used for conditional evaluation of the graph.
"""

from .base import Control
from .functional import compose


class If(Control):

def __init__(self, condition_needs, condition, **kwargs):
super(If, self).__init__(**kwargs)
self.condition_needs = condition_needs
self.condition = condition
self.order = 1

def __call__(self, *args):
self.graph = compose(name=self.name)(*args)
return self

def _compute_condition(self, named_inputs):
inputs = [named_inputs[d] for d in self.condition_needs]
return self.condition(*inputs)

def _compute(self, named_inputs):
return self.graph(named_inputs)


class ElseIf(If):

def __init__(self, condition_needs, condition, **kwargs):
super(ElseIf, self).__init__(condition_needs, condition, **kwargs)
self.order = 2


class Else(Control):

def __init__(self, **kwargs):
super(Else, self).__init__(**kwargs)
self.order = 3

def __call__(self, *args):
self.graph = compose(name=self.name)(*args)
return self

def _compute(self, named_inputs):
return self.graph(named_inputs)
50 changes: 46 additions & 4 deletions graphkit/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from io import StringIO

from .base import Operation
from .base import Operation, Control


class DataPlaceholderNode(str):
Expand Down Expand Up @@ -83,6 +83,10 @@ def add_op(self, operation):
for p in operation.provides:
self.graph.add_edge(operation, DataPlaceholderNode(p))

if isinstance(operation, Control) and hasattr(operation, 'condition_needs'):
for n in operation.condition_needs:
self.graph.add_edge(DataPlaceholderNode(n), operation)

# clear compiled steps (must recompile after adding new layers)
self.steps = []

Expand All @@ -97,6 +101,8 @@ def show_layers(self):
print("\t", "needs: ", step.needs)
print("\t", "provides: ", step.provides)
print("\t", "color: ", step.color)
if hasattr(step, 'condition_needs'):
print("\t", "condition needs: ", step.condition_needs)
print("")

def compile(self):
Expand All @@ -107,14 +113,37 @@ def compile(self):
self.steps = []

# create an execution order such that each layer's needs are provided.
ordered_nodes = list(nx.dag.topological_sort(self.graph))
try:
def key(node):

if hasattr(node, 'order'):
return node.order
elif isinstance(node, DataPlaceholderNode):
return float('-inf')
else:
return 0

ordered_nodes = list(nx.dag.lexicographical_topological_sort(self.graph,
key=key))
except TypeError as e:
if self._debug:
print("Lexicographical topological sort failed! Falling back to topological sort.")

if not any(map(lambda node: isinstance(node, Control), self.graph.nodes)):
ordered_nodes = list(nx.dag.topological_sort(self.graph))
else:
print("Topological sort failed!")
raise e

# add Operations evaluation steps, and instructions to free data.
for i, node in enumerate(ordered_nodes):

if isinstance(node, DataPlaceholderNode):
continue

elif isinstance(node, Control):
self.steps.append(node)

elif isinstance(node, Operation):

# add layer to list of steps
Expand Down Expand Up @@ -256,11 +285,24 @@ def compute(self, outputs, named_inputs, color=None):
# Find the subset of steps we need to run to get to the requested
# outputs from the provided inputs.
all_steps = self._find_necessary_steps(outputs, named_inputs, color)

# import pdb
self.times = {}
if_true = False
for step in all_steps:

if isinstance(step, Operation):
if isinstance(step, Control):
# pdb.set_trace()
if hasattr(step, 'condition'):
if_true = step._compute_condition(cache)
if if_true:
layer_outputs = step._compute(cache)
cache.update(layer_outputs)
elif not if_true:
layer_outputs = step._compute(cache)
cache.update(layer_outputs)
if_true = False

elif isinstance(step, Operation):

if self._debug:
print("-"*32)
Expand Down

10 comments on commit e76a39d

@ankostis
Copy link

@ankostis ankostis commented on e76a39d Oct 8, 2019

Choose a reason for hiding this comment

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

Great to see that you also want conditional nodes in the graph.
I think you should be watching my efforts on this project for the last 2 weeks or so: ae01163

It support an enhanced DAG-solver(yahoo#26) to fix pruning too-much or too-little bugs(yahoo#23, yahoo#24) of this project.
Also operations without all their need covered are implicitly excluded from the execution,
and this should be very relevant here (yahoo#18).

It has many more enhancements, such as

Getting back to "conditionals", how do you deal with conditional outputs?
I mean, what if some output is not produced?

@ankostis
Copy link

Choose a reason for hiding this comment

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

(maybe this diagram will fire up your curiosity)
intro

@ankostis
Copy link

Choose a reason for hiding this comment

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

And this is the legend:
GraphkitLegend

@syamajala
Copy link
Owner Author

Choose a reason for hiding this comment

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

I'm not quite sure I understand the picture. What are evicted nodes? What does the step sequence mean?

As for your question, the way it works is a conditional node overrides __call__ and takes a subgraph. It only executes that subgraph if the condition is true. It looks like there is in fact a bug when a node depends on the output of a conditional branch and that output is not produced.

You can see an example here:
https://github.com/syamajala/graphkit/blob/master/test/test_graphkit.py#L233

Some other features I added in my fork of graphkit were the ability to color nodes of the graph and only execute subgraphs of a specific color and type checking. You can specific an optional type for each input and output in order to assure the graph you are constructing is valid.

Also development of the fork has actually moved to here:
https://github.com/slac-lcls/networkfox

@ankostis
Copy link

@ankostis ankostis commented on e76a39d Oct 8, 2019

Choose a reason for hiding this comment

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

  • Steps sequence: thesteps
  • Eviction: DeleteInstruction
  • Pin: another type of instruction, explained with this graph, where the must run operation produces 2 outputs, but one of them overridden is also an intermediate given-input, so it must be retained, and not overwritten, thus "pinned".
    t
    Look ho

This is the overview of the new dag-solver.
Beyond the docstrings, which rhey took some time to rewrite, the pull-requests linked above also explain stuff.
Finally, the test-cases have been significantly retrofitted. You may insert plot commands to view the situation of each new feature.

It looks like there is in fact a bug when a node depends on the output of a conditional branch and that output is not produced.

I think in know what you mean.
When an operation receives only partial inputs, v1.2.4 crashes (that is the "unsatisfied" in yahoo#18).
It has been fixed in my master.

Regarding your new features, do you have them somewhere documented?

@ankostis
Copy link

Choose a reason for hiding this comment

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

Oups, forgotthe Pin diagram.

@syamajala
Copy link
Owner Author

Choose a reason for hiding this comment

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

@ankostis
Copy link

Choose a reason for hiding this comment

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

Thank you. The colors is an handy extension.

The test-case for the If-Then-Else sparks some questions:

  • What if i have 2 If's or Else's in a same network?

  • Is it user-friendly to write such graphs? Or are you planning for an automated tools producing the rules?

  • Would a generic approach for "optional outputs" cover partially your use-cases?

    I mean, what if it was possible to demarcate that after some operation has completed, the graph must re-schedule because some of its outputs are now definitely not produced?
    Wouldn't it then be more easy to express the If-Then-Else rules in plain python code, and wrap it up in an operation-with-optional-outputs?

@syamajala
Copy link
Owner Author

Choose a reason for hiding this comment

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

Multiple if's, else's should work.

I found that you need to enforce that they happen in the right order. An If should always proceed an Else and an Else should always succeed an If, doing this you can maintain pointers within each node, as you see here:
https://github.com/slac-lcls/networkfox/blob/master/networkfox/functional.py#L236
I ended up removing ElseIf to simplify since we did not have a need for it, but I think the same principal should apply. I believe this also matters when you try to execute the graph in parallel, but I don't remember all the details.

As for whether it is user friendly or not, it seemed like the most intuitive option to me, aside from having to explicitly state what the subgraph needs and provides in the If/Else statement, it mimics what a normal If/Else statement looks like.

We do not write these graphs by hand. We have a gui tool that generates them.
2019-10-09-132739_2158x1894_scrot

It seems like an optional output approach might work though? I'd need to think about it some more.

@ankostis
Copy link

@ankostis ankostis commented on e76a39d Oct 9, 2019

Choose a reason for hiding this comment

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

Thank you. It's a pity that your screenshot does not depict an if-then-else component.

Another approach is retrofit the nodes with an optional "conditional gate" in front of their inputs, and express the if-rules with the nodes producing the same output but different conditions. Then you wouldn't need the 3 kjinds of nodes, correct?
You still need an ordering (akin to a "weight), to command the order of evaluation of conditions, but it is more generic.

I would say that In general,

  • conditional node-execution is needed to express decision-logic across-nodes, and
  • optional outputs are needed to express decision-logic inside a node.

Please sign in to comment.