Skip to content

Commit

Permalink
Merge pull request #2 from wbernoudy/experimental/_model-base-class
Browse files Browse the repository at this point in the history
_Model base class
  • Loading branch information
wbernoudy authored Nov 20, 2024
2 parents f4fa9ae + bc08b4b commit fb3ba6f
Show file tree
Hide file tree
Showing 15 changed files with 892 additions and 612 deletions.
15 changes: 15 additions & 0 deletions dwave/optimization/expression/__init__.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2024 D-Wave Systems Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from dwave.optimization.expression.expression cimport Expression
1 change: 1 addition & 0 deletions dwave/optimization/expression/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from dwave.optimization.expression.expression import Expression
26 changes: 26 additions & 0 deletions dwave/optimization/expression/expression.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# distutils: language = c++

# Copyright 2024 D-Wave Systems Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from dwave.optimization.model cimport ArraySymbol, _Model


__all__ = ["Expression"]


cdef class Expression(_Model):
cdef readonly ArraySymbol output

cpdef Py_ssize_t num_inputs(self) noexcept
37 changes: 37 additions & 0 deletions dwave/optimization/expression/expression.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2024 D-Wave Systems Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Optional

from dwave.optimization.model import _Model


_ShapeLike: typing.TypeAlias = typing.Union[int, collections.abc.Sequence[int]]


class Expression(_Model):
def __init__(
self,
num_inputs: int = 0,
lower_bound: Optional[float] = None,
upper_bound: Optional[float] = None,
integral: Optional[bool] = None,
): ...

def input(self, lower_bound: float, upper_bound: float, integral: bool, shape: Optional[tuple] = None):

@property
def output(self) -> ArraySymbol: ...

def set_output(self, value: ArraySymbol): ...
71 changes: 71 additions & 0 deletions dwave/optimization/expression/expression.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright 2024 D-Wave Systems Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import numbers
from typing import Optional

from libcpp cimport bool
from libcpp.cast cimport dynamic_cast

from dwave.optimization.libcpp.array cimport Array as cppArray
from dwave.optimization.libcpp.graph cimport Node as cppNode
from dwave.optimization.libcpp.nodes cimport InputNode as cppInputNode
from dwave.optimization.model cimport ArraySymbol, _Model, States
from dwave.optimization.symbols cimport symbol_from_ptr

ctypedef cppNode* cppNodePtr


__all__ = ["Expression"]


cdef class Expression(_Model):
def __init__(
self,
num_inputs: int = 0,
# necessary to prevent Cython from rejecting an int
lower_bound: Optional[numbers.Real] = None,
upper_bound: Optional[numbers.Real] = None,
integral: Optional[bool] = None,
):
self.states = States(self)

self._data_sources = []

if num_inputs > 0:
if any(arg is None for arg in (lower_bound, upper_bound, integral)):
raise ValueError(
"`lower_bound`, `upper_bound` and `integral` must be provided "
"explicitly when initializing inputs"
)
for _ in range(num_inputs):
self.input(lower_bound, upper_bound, integral)

def input(self, lower_bound: float, upper_bound: float, bool integral):
"""TODO"""
# avoid circular import
from dwave.optimization.symbols import Input
# Shape is always scalar for now
return Input(self, lower_bound, upper_bound, integral, shape=tuple())

def set_output(self, value: ArraySymbol):
self.output = value

cpdef Py_ssize_t num_inputs(self) noexcept:
return self._graph.num_inputs()

def iter_inputs(self):
inputs = self._graph.inputs()
for i in range(self._graph.num_inputs()):
yield symbol_from_ptr(self, inputs[i])
10 changes: 10 additions & 0 deletions dwave/optimization/include/dwave-optimization/graph.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ namespace dwave::optimization {
class ArrayNode;
class Node;
class DecisionNode;
class InputNode;

// We don't want this interface to be opinionated about what type of rng we're using.
// So we create this class to do type erasure on RNGs.
Expand Down Expand Up @@ -139,6 +140,9 @@ class Graph {
// The number of constraints in the model.
ssize_t num_constraints() const noexcept { return constraints_.size(); }

// The number of input nodes in the model.
ssize_t num_inputs() const noexcept { return inputs_.size(); }

// Specify the objective node. Must be an array with a single element.
// To unset the objective provide nullptr.
void set_objective(ArrayNode* objective_ptr);
Expand All @@ -159,6 +163,9 @@ class Graph {
std::span<DecisionNode* const> decisions() noexcept { return decisions_; }
std::span<const DecisionNode* const> decisions() const noexcept { return decisions_; }

std::span<InputNode* const> inputs() noexcept { return inputs_; }
std::span<const InputNode* const> inputs() const noexcept { return inputs_; }

// Remove unused nodes from the graph.
//
// This method will reset the topological sort if there is one.
Expand All @@ -182,6 +189,7 @@ class Graph {
ArrayNode* objective_ptr_ = nullptr;
std::vector<ArrayNode*> constraints_;
std::vector<DecisionNode*> decisions_;
std::vector<InputNode*> inputs_;

// Track whether the model is currently topologically sorted
bool topologically_sorted_ = false;
Expand Down Expand Up @@ -332,6 +340,8 @@ NodeType* Graph::emplace_node(Args&&... args) {
static_assert(std::is_base_of_v<DecisionNode, NodeType>);
ptr->topological_index_ = decisions_.size();
decisions_.emplace_back(ptr);
} else if constexpr (std::is_base_of_v<InputNode, NodeType>) {
inputs_.emplace_back(ptr);
}

return ptr; // return the observing pointer
Expand Down
14 changes: 13 additions & 1 deletion dwave/optimization/libcpp/graph.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,23 @@ from libcpp.vector cimport vector
from dwave.optimization.libcpp.array cimport Array, span
from dwave.optimization.libcpp.state cimport State


# This seems to be necessary to allow Cython to iterate over the returned
# span from `inputs()` directly. Otherwise it tries to cast it to a non-const
# version of span before iterating, which the C++ compiler will complain about.
ctypedef InputNode* const constInputNodePtr


cdef extern from "dwave-optimization/graph.hpp" namespace "dwave::optimization" nogil:
cdef cppclass Graph:
T* emplace_node[T](...) except+
void initialize_state(State&) except+
span[const unique_ptr[Node]] nodes() const
span[ArrayNode*] constraints() const
Py_ssize_t num_constraints()
Py_ssize_t num_nodes()
Py_ssize_t num_decisions()
Py_ssize_t num_constraints()
Py_ssize_t num_inputs()
@staticmethod
void recursive_initialize(State&, Node*) except+
@staticmethod
Expand All @@ -41,6 +49,7 @@ cdef extern from "dwave-optimization/graph.hpp" namespace "dwave::optimization"
void topological_sort()
bool topologically_sorted() const
Py_ssize_t remove_unused_nodes()
span[constInputNodePtr] inputs()

cdef cppclass Node:
struct SuccessorView:
Expand All @@ -52,3 +61,6 @@ cdef extern from "dwave-optimization/graph.hpp" namespace "dwave::optimization"

cdef cppclass ArrayNode(Node, Array):
pass

cdef cppclass InputNode(Node, Array):
pass
61 changes: 32 additions & 29 deletions dwave/optimization/model.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,10 @@ from dwave.optimization.libcpp.state cimport State as cppState
__all__ = ["Model"]


cdef class Model:
cdef class _Model:
cpdef bool is_locked(self) noexcept
cpdef Py_ssize_t num_decisions(self) noexcept
cpdef Py_ssize_t num_nodes(self) noexcept
cpdef Py_ssize_t num_constraints(self) noexcept


# Allow dynamic attributes on the Model class
cdef dict __dict__

Expand All @@ -40,6 +38,31 @@ cdef class Model:

cdef cppGraph _graph

cdef readonly States states
"""States of the model.
:ref:`States <intro_optimization_states>` represent assignments of values
to a symbol.
See also:
:ref:`States methods <optimization_models>` such as
:meth:`~dwave.optimization.model.States.size` and
:meth:`~dwave.optimization.model.States.resize`.
"""

# The number of times "lock()" has been called.
cdef readonly Py_ssize_t _lock_count

# Used to keep NumPy arrays that own data alive etc etc
# We could pair each of these with an expired_ptr for the node holding
# memory for easier cleanup later if that becomes a concern.
cdef object _data_sources


cdef class Model(_Model):
cpdef Py_ssize_t num_constraints(self) noexcept
cpdef Py_ssize_t num_decisions(self) noexcept

cdef readonly object objective # todo: cdef ArraySymbol?
"""Objective to be minimized.
Expand All @@ -62,26 +85,6 @@ cdef class Model:
Objective = -4.0
"""

cdef readonly States states
"""States of the model.
:ref:`States <intro_optimization_states>` represent assignments of values
to a symbol.
See also:
:ref:`States methods <optimization_models>` such as
:meth:`~dwave.optimization.model.States.size` and
:meth:`~dwave.optimization.model.States.resize`.
"""

# The number of times "lock()" has been called.
cdef readonly Py_ssize_t _lock_count

# Used to keep NumPy arrays that own data alive etc etc
# We could pair each of these with an expired_ptr for the node holding
# memory for easier cleanup later if that becomes a concern.
cdef object _data_sources


cdef class States:
"""The states/solutions of the model."""
Expand All @@ -91,7 +94,7 @@ cdef class States:
cpdef resolve(self)
cpdef Py_ssize_t size(self) except -1

cdef Model _model(self)
cdef _Model _model(self)

# In order to not create a circular reference, we only hold a weakref
# to the model from the states. This introduces some overhead, but it
Expand All @@ -114,20 +117,20 @@ cdef class States:

cdef class Symbol:
# Inheriting nodes must call this method from their __init__()
cdef void initialize_node(self, Model model, cppNode* node_ptr) noexcept
cdef void initialize_node(self, _Model model, cppNode* node_ptr) noexcept

cpdef uintptr_t id(self) noexcept

# Exactly deref(self.expired_ptr)
cpdef bool expired(self) noexcept

@staticmethod
cdef Symbol from_ptr(Model model, cppNode* ptr)
cdef Symbol from_ptr(_Model model, cppNode* ptr)

# Hold on to a reference to the Model, both for access but also, importantly,
# to ensure that the model doesn't get garbage collected unless all of
# the observers have also been garbage collected.
cdef readonly Model model
cdef readonly _Model model

# Hold Node* pointer. This is redundant as most observers will also hold
# a pointer to their observed node with the correct type. But the cost
Expand All @@ -145,7 +148,7 @@ cdef class Symbol:
# also Symbols (probably a fair assumption)
cdef class ArraySymbol(Symbol):
# Inheriting symbols must call this method from their __init__()
cdef void initialize_arraynode(self, Model model, cppArrayNode* array_ptr) noexcept
cdef void initialize_arraynode(self, _Model model, cppArrayNode* array_ptr) noexcept

# Hold ArrayNode* pointer. Again this is redundant, because we're also holding
# a pointer to Node* and we can theoretically dynamic cast each time.
Expand Down
Loading

0 comments on commit fb3ba6f

Please sign in to comment.