-
Notifications
You must be signed in to change notification settings - Fork 18
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
Add NaryReduceNode and InputNode #166
base: main
Are you sure you want to change the base?
Conversation
I tried out some more complicated things to support custom exception handling in Cython. I put it in a separate branch for now as I'm not sure we'll go that direction. Separate PR here to try to make it a bit easier to review wbernoudy#1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly looked at design rather than line-by-line review.
dwave/optimization/include/dwave-optimization/nodes/constants.hpp
Outdated
Show resolved
Hide resolved
class NaryReduceNode : public ArrayOutputMixin<ArrayNode> { | ||
public: | ||
// Runtime constructor that can be used from Cython/Python | ||
NaryReduceNode(Graph&& expression, const std::vector<InputNode*>& inputs, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it makes more sense to not have the user provide inputs etc explicitly, but rather to inspect the expression
. Maybe set_objetive()
sets the output? And we can traverse the nodes looking for InputNode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think inputs should be provided explicitly because they need to be mapped to the operands/predecessors. Otherwise we are relying on the implicit ordering of when the nodes are added to the expression model, and it becomes less clear which one is the "special" input that takes the previous value.
But I still find this interface confusing... was thinking about taking a map between inputs/operands as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the Python level this could perhaps be addressed by not adding an input()
method, but having a subclass of Model
that requires num_inputs
at construction-time. Then we rely on the order? We might have to treat inputs as decision variables in that they get topologically ordered immediately in that case... Needs thought.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that. The ExpressionModel
or whatever (subclass of Model
) could disallow adding decisions.
Perhaps we could also somehow pull a list of supported nodes from C++ and then disallow adding unsupported symbols to ExpressionModel
. This will go a long way to prevent constructing unsupported expressions, as I don't think we will have any supported nodes that can have non-scalar output given all scalar inputs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And I'll note I expect the standard flow when creating expressions is just to create a list of Input
s anyway and use that. So exp = ExpressionModel(7)
, exp.inputs[0] + exp.inputs[1]
feels natural to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that we're now using objective
for the output, and expression.input()
to create the inputs, IMO we should just read the inputs()
and objective()
off the expression rather than providing them explicitly. We can always allow reorderings etc via a method like this later.
fb3ba6f
to
15a5390
Compare
d3c4f79
to
aacee34
Compare
dwave/optimization/include/dwave-optimization/nodes/constants.hpp
Outdated
Show resolved
Hide resolved
@@ -73,6 +74,8 @@ class Graph { | |||
public: | |||
Graph(); | |||
~Graph(); | |||
Graph(Graph&&); | |||
Graph& operator=(Graph&& other) = default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency, we should put this in the .cpp
with the others. Or move the others into the .hpp
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevermind! See #184
dwave/optimization/_model.pyx
Outdated
@@ -185,7 +186,8 @@ cdef class _Graph: | |||
objective_id = json.loads(objective_buff) | |||
if not isinstance(objective_id, int) or objective_id >= model.num_nodes(): | |||
raise ValueError("objective must be an integer and a valid node id") | |||
model.minimize(symbol_from_ptr(model, model._graph.nodes()[objective_id].get())) | |||
model._set_objective(symbol_from_ptr(model, model._graph.nodes()[objective_id].get())) | |||
# model.minimize(symbol_from_ptr(model, model._graph.nodes()[objective_id].get())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leftover comment
dwave/optimization/_model.pyx
Outdated
self.lock() | ||
self.into_file(file, max_num_states=max_num_states, only_decision=only_decision) | ||
self.unlock() | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The context manager version of .lock()
is defined only for Model
currently. I could also add it to Expression
or _Graph
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, excellent point! In which case we should at least do it in a
self.lock()
try:
self.into_file(...)
finally:
self.unlock()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, that's better!
dwave/optimization/_model.pyx
Outdated
>>> model.minimize(y) | ||
""" | ||
def _set_objective(self, ArraySymbol value): | ||
"""Set the objective value on the ``dwave::optimization::Graph``.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be a good place to note that we are using "objective" somewhat generously, and that it can refer to the output of an expression as well.
class NaryReduceNode : public ArrayOutputMixin<ArrayNode> { | ||
public: | ||
// Runtime constructor that can be used from Cython/Python | ||
NaryReduceNode(Graph&& expression, const std::vector<InputNode*>& inputs, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that we're now using objective
for the output, and expression.input()
to create the inputs, IMO we should just read the inputs()
and objective()
off the expression rather than providing them explicitly. We can always allow reorderings etc via a method like this later.
69bbd65
to
be9f5ee
Compare
`NaryReduce`). | ||
Args: | ||
expression: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just noticed that numpy.ufunc.reduce(), functools.reduce() and std::accumulate() all use the left-most argument for the init/accumulated value. So I think we should follow that convention by making it the first input.
For completeness std::reduce() requires commutative and associative, which we definitely don't want to require.
be65ccc
to
23cefb1
Compare
* use span instead of vector in a few places where appropriate * fix bugs found in code review
and call in relevant methods
3a45e03
to
f60e6d7
Compare
Adds
InputNode
which subclassesConstantNode
but allows assigning of the state after initialization.This is to support the new
NaryReduceNode
, which does an n-ary reduce operation on an arbitrary number of inputs/predecessors. The node is constructed by providing an expression, formulated as a separateGraph
, and initial values for the reduction operation.The current design accepts any
Graph
but then throws exceptions if it contains unsupported nodes. It would be best if we can pass along helpful error messages to users at the python level. Currently this works by encoding the error message and aNode*
in a simple JSON string that is then parsed from Python, where we can grab the corresponding symbol.