diff --git a/dwave/optimization/model.pxd b/dwave/optimization/graph_manager.pxd similarity index 70% rename from dwave/optimization/model.pxd rename to dwave/optimization/graph_manager.pxd index 172fe1aa..5e669d41 100644 --- a/dwave/optimization/model.pxd +++ b/dwave/optimization/graph_manager.pxd @@ -23,56 +23,20 @@ from dwave.optimization.libcpp.graph cimport ArrayNode as cppArrayNode, Node as from dwave.optimization.libcpp.graph cimport Graph as cppGraph from dwave.optimization.libcpp.state cimport State as cppState -__all__ = ["Model"] +cdef class _GraphManager: + cpdef bool _is_locked(self) noexcept + cpdef Py_ssize_t _num_nodes(self) noexcept -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 + # Allow dynamic attributes on the _GraphManager class cdef dict __dict__ - # Make the Model class weak referenceable + # Make the _GraphManager class weak referenceable cdef object __weakref__ cdef cppGraph _graph - cdef readonly object objective # todo: cdef ArraySymbol? - """Objective to be minimized. - - Examples: - This example prints the value of the objective of a model representing - the simple polynomial, :math:`y = i^2 - 4i`, for a state with value - :math:`i=2.0`. - - >>> from dwave.optimization import Model - ... - >>> model = Model() - >>> i = model.integer(lower_bound=-5, upper_bound=5) - >>> c = model.constant(4) - >>> y = i**2 - c*i - >>> model.minimize(y) - >>> with model.lock(): - ... model.states.resize(1) - ... i.set_state(0, 2.0) - ... print(f"Objective = {model.objective.state(0)}") - Objective = -4.0 - """ - - cdef readonly States states - """States of the model. - - :ref:`States ` represent assignments of values - to a symbol. - - See also: - :ref:`States methods ` such as - :meth:`~dwave.optimization.model.States.size` and - :meth:`~dwave.optimization.model.States.resize`. - """ + cdef readonly States _states # The number of times "lock()" has been called. cdef readonly Py_ssize_t _lock_count @@ -82,6 +46,11 @@ cdef class Model: # memory for easier cleanup later if that becomes a concern. cdef object _data_sources + cpdef Py_ssize_t _num_constraints(self) noexcept + cpdef Py_ssize_t _num_decisions(self) noexcept + + cdef readonly object _objective # todo: cdef ArraySymbol? + cdef class States: """The states/solutions of the model.""" @@ -91,7 +60,8 @@ cdef class States: cpdef resolve(self) cpdef Py_ssize_t size(self) except -1 - cdef Model _model(self) + # TODO: rename this to _graph_manager? + cdef _GraphManager _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 @@ -114,7 +84,7 @@ 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, _GraphManager model, cppNode* node_ptr) noexcept cpdef uintptr_t id(self) noexcept @@ -122,12 +92,12 @@ cdef class Symbol: cpdef bool expired(self) noexcept @staticmethod - cdef Symbol from_ptr(Model model, cppNode* ptr) + cdef Symbol from_ptr(_GraphManager 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 _GraphManager 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 @@ -145,7 +115,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, _GraphManager 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. diff --git a/dwave/optimization/graph_manager.pyi b/dwave/optimization/graph_manager.pyi new file mode 100644 index 00000000..5fd6e189 --- /dev/null +++ b/dwave/optimization/graph_manager.pyi @@ -0,0 +1,180 @@ +# 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 collections.abc +import typing + +import numpy +import numpy.typing + +from dwave.optimization.model import Model +from dwave.optimization.symbols import ( + Absolute, + Add, + AdvancedIndexing, + All, + Any, + BasicIndexing, + Constant, + Equal, + LessEqual, + Max, + Min, + Modulus, + Multiply, + NaryAdd, + NaryMultiply, + Negative, + PartialSum, + Permutation, + Prod, + Reshape, + Size, + Subtract, + Sum, +) + +_ShapeLike: typing.TypeAlias = typing.Union[int, collections.abc.Sequence[int]] + +_GraphManagerT = typing.TypeVar('_GraphManagerT', bound='_GraphManager') + + +class _GraphManager: + def __init__(self): ... + + @property + def _states(self) -> "States": ... + def _constant(self, array_like: numpy.typing.ArrayLike) -> Constant: ... + + @classmethod + def _from_file( + cls: typing.Type[_GraphManagerT], + file: typing.Union[typing.BinaryIO, collections.abc.ByteString, str], + *, + check_header: bool = True, + ) -> _GraphManagerT: ... + + def _into_file( + self, + file: typing.Union[typing.BinaryIO, collections.abc.ByteString, str], + *, + max_num_states: int = 0, + only_decision: bool = False, + ): ... + def _is_locked(self) -> bool: ... + def _iter_symbols(self) -> collections.abc.Iterator["Symbol"]: ... + def _num_nodes(self) -> int: ... + def _num_symbols(self) -> int: ... + def _remove_unused_symbols(self) -> int: ... + def _state_size(self) -> int: ... + + def _to_file( + self, + *, + max_num_states: int = 0, + only_decision: bool = False, + ) -> typing.BinaryIO: ... + + # networkx might not be installed, so we just say we return an object. + def _to_networkx(self) -> object: ... + def _unlock(self): ... + + @property + def _objective(self) -> "ArraySymbol": ... + def _add_constraint(self, value: "ArraySymbol") -> "ArraySymbol": ... + def _iter_constraints(self) -> collections.abc.Iterator["ArraySymbol"]: ... + def _iter_decisions(self) -> collections.abc.Iterator["Symbol"]: ... + def _minimize(self, value: "ArraySymbol"): ... + def _num_constraints(self) -> int: ... + def _num_decisions(self) -> int: ... + + +class States: + def __init__(self, model: Model): ... + def __len__(self) -> int: ... + def clear(self): ... + + def from_file( + self, + file: typing.Union[typing.BinaryIO, collections.abc.ByteString, str], + *, + replace: bool = True, + check_header: bool = True, + ) -> Model: ... + def from_future(self, future: object, result_hook: collections.abc.Callable): ... + def initialize(self): ... + + def into_file( + self, + file: typing.Union[typing.BinaryIO, collections.abc.ByteString, str], + ): ... + + def resize(self, n: int): ... + def resolve(self): ... + def size(self) -> int: ... + def to_file(self) -> typing.BinaryIO: ... + + +class Symbol: + def __init__(self, *args, **kwargs) -> typing.NoReturn: ... + def equals(self, other: "Symbol") -> bool: ... + def expired(self) -> bool: ... + def has_state(self, index: int = 0) -> bool: ... + def id(self) -> int: ... + def iter_predecessors(self) -> collections.abc.Iterator["Symbol"]: ... + def iter_successors(self) -> collections.abc.Iterator["Symbol"]: ... + def maybe_equals(self, other: "Symbol") -> int: ... + def reset_state(self, index: int): ... + def shares_memory(self, other: "Symbol") -> bool: ... + def state_size(self) -> int: ... + def topological_index(self) -> int: ... + + +class ArraySymbol(Symbol): + def __init__(self, *args, **kwargs) -> typing.NoReturn: ... + def __abs__(self) -> Absolute: ... + def __add__(self, rhs: "ArraySymbol") -> Add: ... + def __bool__(self) -> typing.NoReturn: ... + def __eq__(self, rhs: "ArraySymbol") -> Equal: ... # type: ignore + + def __getitem__( + self, + index: typing.Union[Symbol, int, slice, tuple], + ) -> typing.Union[AdvancedIndexing, BasicIndexing, Permutation]: ... + + def __iadd__(self, rhs: "ArraySymbol") -> NaryAdd: ... + def __imul__(self, rhs: "ArraySymbol") -> NaryMultiply: ... + def __le__(self, rhs: "ArraySymbol") -> LessEqual: ... + def __mod__(self, rhs: "ArraySymbol") -> Modulus: ... + def __mul__(self, rhs: "ArraySymbol") -> Multiply: ... + def __neg__(self) -> Negative: ... + def __pow__(self, exponent: int) -> "ArraySymbol": ... + def __sub__(self, rhs: "ArraySymbol") -> Subtract: ... + def all(self) -> All: ... + def any(self) -> Any: ... + def max(self) -> Max: ... + def min(self) -> Min: ... + def ndim(self) -> int: ... + def prod(self) -> Prod: ... + def reshape(self, shape: _ShapeLike) -> Reshape: ... + def shape(self) -> tuple[int, ...]: ... + def size(self) -> typing.Union[int, Size]: ... + def sqrt(self) -> "ArraySymbol": ... + def state(self, index: int = 0, *, copy: bool = True) -> numpy.ndarray: ... + def state_size(self) -> int: ... + def strides(self) -> tuple[int, ...]: ... + + def sum( + self, axis: typing.Optional[int] = None + ) -> typing.Union[Sum, PartialSum]: ... diff --git a/dwave/optimization/model.pyx b/dwave/optimization/graph_manager.pyx similarity index 80% rename from dwave/optimization/model.pyx rename to dwave/optimization/graph_manager.pyx index 43e666a3..52a4512c 100644 --- a/dwave/optimization/model.pyx +++ b/dwave/optimization/graph_manager.pyx @@ -1,3 +1,5 @@ +# distutils: language = c++ + # Copyright 2024 D-Wave Systems Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -49,101 +51,22 @@ from dwave.optimization.libcpp.graph cimport DecisionNode as cppDecisionNode from dwave.optimization.symbols cimport symbol_from_ptr -__all__ = ["Model"] - - @contextlib.contextmanager def locked(model): """Context manager that hold a locked model and unlocks it when the context is exited.""" try: yield finally: - model.unlock() - - -cdef class Model: - """Nonlinear model. + model._unlock() - The nonlinear model represents a general optimization problem with an - :term:`objective function` and/or constraints over variables of various - types. - The :class:`.Model` class can contain this model and its methods provide - convenient utilities for working with representations of a problem. - - Examples: - This example creates a model for a - :class:`flow-shop-scheduling ` - problem with two jobs on three machines. - - >>> from dwave.optimization.generators import flow_shop_scheduling - ... - >>> processing_times = [[10, 5, 7], [20, 10, 15]] - >>> model = flow_shop_scheduling(processing_times=processing_times) - """ +cdef class _GraphManager: def __init__(self): - self.states = States(self) + self._states = States(self) self._data_sources = [] - def add_constraint(self, ArraySymbol value): - """Add a constraint to the model. - - Args: - value: Value that must evaluate to True for the state - of the model to be feasible. - - Returns: - The constraint symbol. - - Examples: - This example adds a single constraint to a model. - - >>> from dwave.optimization.model import Model - >>> model = Model() - >>> i = model.integer() - >>> c = model.constant(5) - >>> constraint_sym = model.add_constraint(i <= c) - - The returned constraint symbol can be assigned and evaluated - for a model state: - - >>> with model.lock(): - ... model.states.resize(1) - ... i.set_state(0, 1) # Feasible state - ... print(constraint_sym.state(0)) - 1.0 - >>> with model.lock(): - ... i.set_state(0, 6) # Infeasible state - ... print(constraint_sym.state(0)) - 0.0 - """ - if value is None: - raise ValueError("value cannot be None") - # TODO: shall we accept array valued constraints? - self._graph.add_constraint(value.array_ptr) - return value - - def binary(self, shape=None): - r"""Create a binary symbol as a decision variable. - - Args: - shape: Shape of the binary array to create. - - Returns: - A binary symbol. - - Examples: - This example creates a :math:`1 \times 20`-sized binary symbol. - - >>> from dwave.optimization.model import Model - >>> model = Model() - >>> x = model.binary((1,20)) - """ - from dwave.optimization.symbols import BinaryVariable #avoid circular import - return BinaryVariable(self, shape) - - def constant(self, array_like): + def _constant(self, array_like): r"""Create a constant symbol. Args: @@ -165,119 +88,11 @@ cdef class Model: from dwave.optimization.symbols import Constant # avoid circular import return Constant(self, array_like) - def decision_state_size(self): - r"""An estimated size, in bytes, of the model's decision states. - - Examples: - This example checks the size of a model with one - :math:`10 \times 10`-sized integer symbol. - - >>> from dwave.optimization.model import Model - >>> model = Model() - >>> visit_site = model.integer((10, 10)) - >>> model.decision_state_size() - 800 - """ - return sum(sym.state_size() for sym in self.iter_decisions()) - - def disjoint_bit_sets(self, Py_ssize_t primary_set_size, Py_ssize_t num_disjoint_sets): - """Create a disjoint-sets symbol as a decision variable. - - Divides a set of the elements of ``range(primary_set_size)`` into - ``num_disjoint_sets`` ordered partitions, stored as bit sets (arrays - of length ``primary_set_size``, with ones at the indices of elements - currently in the set, and zeros elsewhere). The ordering of a set is - not semantically meaningful. - - Also creates from the symbol ``num_disjoint_sets`` extra successors - that output the disjoint sets as arrays. - - Args: - primary_set_size: Number of elements in the primary set that are - partitioned into disjoint sets. Must be non-negative. - num_disjoint_sets: Number of disjoint sets. Must be positive. - - Returns: - A tuple where the first element is the disjoint-sets symbol and - the second is a set of its newly added successors. - - Examples: - This example creates a symbol of 10 elements that is divided - into 4 sets. - - >>> from dwave.optimization.model import Model - >>> model = Model() - >>> parts_set, parts_subsets = model.disjoint_bit_sets(10, 4) - """ - - from dwave.optimization.symbols import DisjointBitSets, DisjointBitSet # avoid circular import - main = DisjointBitSets(self, primary_set_size, num_disjoint_sets) - sets = tuple(DisjointBitSet(main, i) for i in range(num_disjoint_sets)) - return main, sets - - def disjoint_lists(self, Py_ssize_t primary_set_size, Py_ssize_t num_disjoint_lists): - """Create a disjoint-lists symbol as a decision variable. - - Divides a set of the elements of ``range(primary_set_size)`` into - ``num_disjoint_lists`` ordered partitions. - - Also creates ``num_disjoint_lists`` extra successors from the - symbol that output the disjoint lists as arrays. - - Args: - primary_set_size: Number of elements in the primary set to - be partitioned into disjoint lists. - num_disjoint_lists: Number of disjoint lists. - - Returns: - A tuple where the first element is the disjoint-lists symbol - and the second is a list of its newly added successor nodes. - - Examples: - This example creates a symbol of 10 elements that is divided - into 4 lists. - - >>> from dwave.optimization.model import Model - >>> model = Model() - >>> destinations, routes = model.disjoint_lists(10, 4) - """ - from dwave.optimization.symbols import DisjointLists, DisjointList # avoid circular import - main = DisjointLists(self, primary_set_size, num_disjoint_lists) - lists = [DisjointList(main, i) for i in range(num_disjoint_lists)] - return main, lists - - def feasible(self, int index = 0): - """Check the feasibility of the state at the input index. - - Args: - index: index of the state to check for feasibility. - - Returns: - Feasibility of the state. - - Examples: - This example demonstrates checking the feasibility of a simple model with - feasible and infeasible states. - - >>> from dwave.optimization.model import Model - >>> model = Model() - >>> b = model.binary() - >>> model.add_constraint(b) # doctest: +ELLIPSIS - - >>> model.states.resize(2) - >>> b.set_state(0, 1) # Feasible - >>> b.set_state(1, 0) # Infeasible - >>> with model.lock(): - ... model.feasible(0) - True - >>> with model.lock(): - ... model.feasible(1) - False - """ - return all(sym.state(index) for sym in self.iter_constraints()) + def _decision_state_size(self): + return sum(sym.state_size() for sym in self._iter_decisions()) @classmethod - def from_file(cls, file, *, + def _from_file(cls, file, *, check_header = True, ): """Construct a model from the given file. @@ -314,7 +129,7 @@ cdef class Model: header_len = struct.unpack('= model.num_nodes(): + 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._minimize(symbol_from_ptr(model, model._graph.nodes()[objective_id].get())) for cid in json.loads(zf.read("constraints.json")): - model.add_constraint(symbol_from_ptr(model, model._graph.nodes()[cid].get())) + model._add_constraint(symbol_from_ptr(model, model._graph.nodes()[cid].get())) # Read any states that have been encoded num_states = model_info.get("num_states") @@ -365,11 +180,11 @@ cdef class Model: raise ValueError("expected num_states to be a positive integer") if num_states > 0: - model.states.resize(num_states) + model._states.resize(num_states) # now read the states of the decision variables - num_decisions = model.num_decisions() # use the model not the serialization - for node in itertools.islice(model.iter_symbols(), 0, num_decisions): + num_decisions = model._num_decisions() # use the model not the serialization + for node in itertools.islice(model._iter_symbols(), 0, num_decisions): for i in range(num_states): node._state_from_zipfile(zf, f"nodes/{node.topological_index()}/states/{i}/", i) @@ -384,39 +199,13 @@ cdef class Model: return model - def integer(self, shape=None, lower_bound=None, upper_bound=None): - r"""Create an integer symbol as a decision variable. - - Args: - shape: Shape of the integer array to create. - - lower_bound: Lower bound for the symbol, which is the - smallest allowed integer value. If None, the default - value is used. - upper_bound: Upper bound for the symbol, which is the - largest allowed integer value. If None, the default - value is used. - - Returns: - An integer symbol. - - Examples: - This example creates a :math:`20 \times 20`-sized integer symbol. - - >>> from dwave.optimization.model import Model - >>> model = Model() - >>> i = model.integer((20,20), lower_bound=-100, upper_bound=100) - """ - from dwave.optimization.symbols import IntegerVariable #avoid circular import - return IntegerVariable(self, shape, lower_bound, upper_bound) - def _header_data(self, *, only_decision, max_num_states=float('inf')): """The header data associated with the model (but not the states).""" - num_nodes = self.num_decisions() if only_decision else self.num_nodes() - num_states = max(0, min(self.states.size(), max_num_states)) + num_nodes = self._num_decisions() if only_decision else self._num_nodes() + num_states = max(0, min(self._states.size(), max_num_states)) - decision_state_size = self.decision_state_size() - state_size = decision_state_size if only_decision else self.state_size() + decision_state_size = self._decision_state_size() + state_size = decision_state_size if only_decision else self._state_size() return dict( decision_state_size=decision_state_size, @@ -425,7 +214,7 @@ cdef class Model: num_states=num_states, ) - def into_file(self, file, *, + def _into_file(self, file, *, Py_ssize_t max_num_states = 0, bool only_decision = False, ): @@ -449,14 +238,14 @@ cdef class Model: TODO: describe the format """ - if not self.is_locked(): + if not self._is_locked(): # lock for the duration of the method - with self.lock(): - return self.into_file(file, max_num_states=max_num_states, only_decision=only_decision) + with self._lock(): + return self._into_file(file, max_num_states=max_num_states, only_decision=only_decision) if isinstance(file, str): with open(file, "wb") as f: - return self.into_file( + return self._into_file( f, max_num_states=max_num_states, only_decision=only_decision, @@ -504,11 +293,11 @@ cdef class Model: # If we're only encoding the decision variables then we want to stop early. # We know that we're topologically sorted so the first num_decisions are # exactly the decision variables. - stop = self.num_decisions() if only_decision else self.num_nodes() + stop = self._num_decisions() if only_decision else self._num_nodes() # On the first pass we made a nodetypes.txt file that has the node names with zf.open("nodetypes.txt", "w", force_zip64=True) as f: - for node in itertools.islice(self.iter_symbols(), 0, stop): + for node in itertools.islice(self._iter_symbols(), 0, stop): f.write(type(node).__name__.encode("UTF-8")) f.write(b"\n") @@ -517,8 +306,8 @@ cdef class Model: # We don't actually need to make the Python symbols here, but it's convenient # Also, if we're only_decision then there will never be predecessors, but # let's reuse the code for now. - stop = self.num_decisions() if only_decision else self.num_nodes() - for node in itertools.islice(self.iter_symbols(), 0, stop): + stop = self._num_decisions() if only_decision else self._num_nodes() + for node in itertools.islice(self._iter_symbols(), 0, stop): f.write(f"{node.topological_index()}".encode("UTF-8")) for pred in node.iter_predecessors(): f.write(f" {pred.topological_index()}".encode("UTF-8")) @@ -526,31 +315,31 @@ cdef class Model: # On the third pass, we allow nodes to save whatever info they want # to in a nested node/ directory - for node in itertools.islice(self.iter_symbols(), 0, stop): + for node in itertools.islice(self._iter_symbols(), 0, stop): directory = f"nodes/{node.topological_index()}/" node._into_zipfile(zf, directory) # Encode the objective and the constraints - if self.objective is not None and self.objective.topological_index() < stop: - zf.writestr("objective.json", encoder.encode(self.objective.topological_index())) + if self._objective is not None and self._objective.topological_index() < stop: + zf.writestr("objective.json", encoder.encode(self._objective.topological_index())) else: zf.writestr("objective.json", b"") constraints = [] # todo: not yet available at the python level - for c in self.iter_constraints(): + for c in self._iter_constraints(): if c is not None and c.topological_index() < stop: constraints.append(c.topological_index()) zf.writestr("constraints.json", encoder.encode(constraints)) # Encode the states if requested if num_states > 0: # redundant, but good short circuit - for node in itertools.islice(self.iter_symbols(), self.num_decisions()): + for node in itertools.islice(self._iter_symbols(), self._num_decisions()): # only save states that have been initialized for i in filter(node.has_state, range(num_states)): directory = f"nodes/{node.topological_index()}/states/{i}/" node._state_into_zipfile(zf, directory, i) - cpdef bool is_locked(self) noexcept: + cpdef bool _is_locked(self) noexcept: """Lock status of the model. No new symbols can be added to a locked model. @@ -560,41 +349,7 @@ cdef class Model: """ return self._lock_count > 0 - def iter_constraints(self): - """Iterate over all constraints in the model. - - Examples: - This example adds a single constraint to a model and iterates over it. - - >>> from dwave.optimization.model import Model - >>> model = Model() - >>> i = model.integer() - >>> c = model.constant(5) - >>> model.add_constraint(i <= c) # doctest: +ELLIPSIS - - >>> constraints = next(model.iter_constraints()) - """ - for ptr in self._graph.constraints(): - yield symbol_from_ptr(self, ptr) - - def iter_decisions(self): - """Iterate over all decision variables in the model. - - Examples: - This example adds a single decision symbol to a model and iterates over it. - - >>> from dwave.optimization.model import Model - >>> model = Model() - >>> i = model.integer() - >>> c = model.constant(5) - >>> model.add_constraint(i <= c) # doctest: +ELLIPSIS - - >>> decisions = next(model.iter_decisions()) - """ - for ptr in self._graph.decisions(): - yield symbol_from_ptr(self, ptr) - - def iter_symbols(self): + def _iter_symbols(self): """Iterate over all symbols in the model. Examples: @@ -612,26 +367,7 @@ cdef class Model: for i in range(nodes.size()): yield symbol_from_ptr(self, nodes[i].get()) - def list(self, n : int): - """Create a list symbol as a decision variable. - - Args: - n: Values in the list are permutations of ``range(n)``. - - Returns: - A list symbol. - - Examples: - This example creates a list symbol of 200 elements. - - >>> from dwave.optimization.model import Model - >>> model = Model() - >>> routes = model.list(200) - """ - from dwave.optimization.symbols import ListVariable # avoid circular import - return ListVariable(self, n) - - def lock(self): + def _lock(self): """Lock the model. No new symbols can be added to a locked model. @@ -675,78 +411,7 @@ cdef class Model: return locked(self) - def minimize(self, ArraySymbol value): - """Set the objective value to minimize. - - Optimization problems have an objective and/or constraints. The objective - expresses one or more aspects of the problem that should be minimized - (equivalent to maximization when multiplied by a minus sign). For example, - an optimized itinerary might minimize the value of distance traveled or - cost of transportation or travel time. - - Args: - value: Value for which to minimize the cost function. - - Examples: - This example minimizes a simple polynomial, :math:`y = i^2 - 4i`, - within bounds. - - >>> from dwave.optimization import Model - >>> model = Model() - >>> i = model.integer(lower_bound=-5, upper_bound=5) - >>> c = model.constant(4) - >>> y = i*i - c*i - >>> model.minimize(y) - """ - if value is None: - raise ValueError("value cannot be None") - if value.size() < 1: - raise ValueError("the value of an empty array is ambiguous") - if value.size() > 1: - raise ValueError("the value of an array with more than one element is ambiguous") - self._graph.set_objective(value.array_ptr) - self.objective = value - - cpdef Py_ssize_t num_constraints(self) noexcept: - """Number of constraints in the model. - - Examples: - This example checks the number of constraints in the model after - adding a couple of constraints. - - >>> from dwave.optimization.model import Model - >>> model = Model() - >>> i = model.integer() - >>> c = model.constant([5, -14]) - >>> model.add_constraint(i <= c[0]) # doctest: +ELLIPSIS - - >>> model.add_constraint(c[1] <= i) # doctest: +ELLIPSIS - - >>> model.num_constraints() - 2 - """ - return self._graph.num_constraints() - - cpdef Py_ssize_t num_decisions(self) noexcept: - """Number of independent decision nodes in the model. - - An array-of-integers symbol, for example, counts as a single - decision node. - - Examples: - This example checks the number of decisions in a model after - adding a single (size 20) decision symbol. - - >>> from dwave.optimization.model import Model - >>> model = Model() - >>> c = model.constant([1, 5, 8.4]) - >>> i = model.integer(20, upper_bound=100) - >>> model.num_decisions() - 1 - """ - return self._graph.num_decisions() - - def num_edges(self): + def _num_edges(self): """Number of edges in the directed acyclic graph for the model. Examples: @@ -767,7 +432,7 @@ cdef class Model: num_edges += self._graph.nodes()[i].get().successors().size() return num_edges - cpdef Py_ssize_t num_nodes(self) noexcept: + cpdef Py_ssize_t _num_nodes(self) noexcept: """Number of nodes in the directed acyclic graph for the model. See also: @@ -787,7 +452,7 @@ cdef class Model: """ return self._graph.num_nodes() - def num_symbols(self): + def _num_symbols(self): """Number of symbols tracked by the model. Equivalent to the number of nodes in the directed acyclic @@ -808,35 +473,9 @@ cdef class Model: >>> model.num_symbols() 2 """ - return self.num_nodes() - - def quadratic_model(self, ArraySymbol x, quadratic, linear=None): - """Create a quadratic model from an array and a quadratic model. - - Args: - x: An array. - - quadratic: Quadratic values for the quadratic model. - - linear: Linear values for the quadratic model. - - Returns: - A quadratic model. - - Examples: - This example creates a quadratic model. - - >>> from dwave.optimization.model import Model - >>> model = Model() - >>> x = model.binary(3) - >>> Q = {(0, 0): 0, (0, 1): 1, (0, 2): 2, (1, 1): 1, (1, 2): 3, (2, 2): 2} - >>> qm = model.quadratic_model(x, Q) - - """ - from dwave.optimization.symbols import QuadraticModel - return QuadraticModel(x, quadratic, linear) + return self._num_nodes() - def remove_unused_symbols(self): + def _remove_unused_symbols(self): """Remove unused symbols from the model. A symbol is considered unused if all of the following are true : @@ -885,33 +524,11 @@ cdef class Model: 3 """ - if self.is_locked(): + if self._is_locked(): raise ValueError("cannot remove symbols from a locked model") return self._graph.remove_unused_nodes() - def set(self, Py_ssize_t n, Py_ssize_t min_size = 0, max_size = None): - """Create a set symbol as a decision variable. - - Args: - n: Values in the set are subsets of ``range(n)``. - min_size: Minimum set size. Defaults to ``0``. - max_size: Maximum set size. Defaults to ``n``. - - Returns: - A set symbol. - - Examples: - This example creates a set symbol of up to 4 elements - with values between 0 to 99. - - >>> from dwave.optimization.model import Model - >>> model = Model() - >>> destinations = model.set(100, max_size=4) - """ - from dwave.optimization.symbols import SetVariable # avoid circular import - return SetVariable(self, n, min_size, n if max_size is None else max_size) - - def state_size(self): + def _state_size(self): """An estimate of the size, in bytes, of all states in the model. Iterates over the model's states and totals the sizes of all. @@ -926,20 +543,20 @@ cdef class Model: >>> model.state_size() 184 """ - return sum(sym.state_size() for sym in self.iter_symbols()) + return sum(sym.state_size() for sym in self._iter_symbols()) - def to_file(self, **kwargs): + def _to_file(self, **kwargs): """Serialize the model to a new file-like object. See also: :meth:`.into_file`, :meth:`.from_file` """ file = tempfile.TemporaryFile(mode="w+b") - self.into_file(file, **kwargs) + self._into_file(file, **kwargs) file.seek(0) return file - def to_networkx(self): + def _to_networkx(self): """Convert the model to a NetworkX graph. Returns: @@ -982,35 +599,35 @@ cdef class Model: G = networkx.MultiDiGraph() # Add the symbols, in order if we happen to be topologically sorted - G.add_nodes_from(repr(symbol) for symbol in self.iter_symbols()) + G.add_nodes_from(repr(symbol) for symbol in self._iter_symbols()) # Sanity check. If several nodes map to the same symbol repr we'll see # too few nodes in the graph - if len(G) != self.num_symbols(): + if len(G) != self._num_symbols(): raise RuntimeError("symbol repr() is not unique to the underlying node") # Now add the edges - for symbol in self.iter_symbols(): + for symbol in self._iter_symbols(): for successor in symbol.iter_successors(): G.add_edge(repr(symbol), repr(successor)) # Sanity check again. If the repr of symbols isn't unique to the underlying # node then we'll see too many nodes in the graph here - if len(G) != self.num_symbols(): + if len(G) != self._num_symbols(): raise RuntimeError("symbol repr() is not unique to the underlying node") # Add the objective if it's present. We call it "minimize" to be # consistent with the minimize() function - if self.objective is not None: - G.add_edge(repr(self.objective), "minimize") + if self._objective is not None: + G.add_edge(repr(self._objective), "minimize") # Likewise if we have constraints, add a special node for them - for symbol in self.iter_constraints(): + for symbol in self._iter_constraints(): G.add_edge(repr(symbol), "constraint(s)") return G - def unlock(self): + def _unlock(self): """Release a lock, decrementing the lock count. Symbols can be added to unlocked models only. @@ -1027,10 +644,101 @@ cdef class Model: # non-decision states if self._lock_count < 1: self._graph.reset_topological_sort() - for i in range(self.states.size()): + for i in range(self._states.size()): # this might actually increase the size of the states in some # cases, but that's fine - self.states._states[i].resize(self.num_decisions()) + self._states._states[i].resize(self._num_decisions()) + + + def _add_constraint(self, ArraySymbol value): + if value is None: + raise ValueError("value cannot be None") + # TODO: shall we accept array valued constraints? + self._graph.add_constraint(value.array_ptr) + return value + + def _iter_constraints(self): + """Iterate over all constraints in the model. + + Examples: + This example adds a single constraint to a model and iterates over it. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> i = model.integer() + >>> c = model.constant(5) + >>> model.add_constraint(i <= c) # doctest: +ELLIPSIS + + >>> constraints = next(model.iter_constraints()) + """ + for ptr in self._graph.constraints(): + yield symbol_from_ptr(self, ptr) + + def _iter_decisions(self): + """Iterate over all decision variables in the model. + + Examples: + This example adds a single decision symbol to a model and iterates over it. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> i = model.integer() + >>> c = model.constant(5) + >>> model.add_constraint(i <= c) # doctest: +ELLIPSIS + + >>> decisions = next(model.iter_decisions()) + """ + for ptr in self._graph.decisions(): + yield symbol_from_ptr(self, ptr) + + def _minimize(self, ArraySymbol value): + if value is None: + raise ValueError("value cannot be None") + if value.size() < 1: + raise ValueError("the value of an empty array is ambiguous") + if value.size() > 1: + raise ValueError("the value of an array with more than one element is ambiguous") + self._graph.set_objective(value.array_ptr) + self._objective = value + + cpdef Py_ssize_t _num_constraints(self) noexcept: + """Number of constraints in the model. + + Examples: + This example checks the number of constraints in the model after + adding a couple of constraints. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> i = model.integer() + >>> c = model.constant([5, -14]) + >>> model.add_constraint(i <= c[0]) # doctest: +ELLIPSIS + + >>> model.add_constraint(c[1] <= i) # doctest: +ELLIPSIS + + >>> model.num_constraints() + 2 + """ + return self._graph.num_constraints() + + cpdef Py_ssize_t _num_decisions(self) noexcept: + """Number of independent decision nodes in the model. + + An array-of-integers symbol, for example, counts as a single + decision node. + + Examples: + This example checks the number of decisions in a model after + adding a single (size 20) decision symbol. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> c = model.constant([1, 5, 8.4]) + >>> i = model.integer(20, upper_bound=100) + >>> model.num_decisions() + 1 + """ + return self._graph.num_decisions() cdef class States: @@ -1085,7 +793,8 @@ cdef class States: >>> model.states.size() 0 """ - def __init__(self, Model model): + + def __init__(self, model): self._model_ref = weakref.ref(model) def __len__(self): @@ -1161,6 +870,10 @@ cdef class States: Returns: A model. """ + + # avoid circular import + from dwave.optimization.model import Model + self.resolve() if not replace: @@ -1168,15 +881,15 @@ cdef class States: # todo: we don't need to actually construct a model, but this is nice and # abstract. We should performance test and then potentially re-implement - cdef Model model = Model.from_file(file, check_header=check_header) + cdef _GraphManager model = _GraphManager._from_file(file, check_header=check_header) # Check that the model is compatible - for n0, n1 in zip(model.iter_symbols(), self._model().iter_symbols()): + for n0, n1 in zip(model._iter_symbols(), self._model()._iter_symbols()): # todo: replace with proper node quality testing once we have it if not isinstance(n0, type(n1)): raise ValueError("cannot load states into a model with mismatched decisions") - self.attach_states(move(model.states.detach_states())) + self.attach_states(move(model._states.detach_states())) def from_future(self, future, result_hook): """Populate the states from the result of a future computation. @@ -1199,9 +912,9 @@ cdef class States: """Initialize any uninitialized states.""" self.resolve() - cdef Model model = self._model() + cdef _GraphManager model = self._model() - if not model.is_locked(): + if not model._is_locked(): raise ValueError("Cannot initialize states of an unlocked model") for i in range(self._states.size()): self._states[i].resize(model.num_nodes()) @@ -1219,12 +932,12 @@ cdef class States: TODO: describe the format """ self.resolve() - return self._model().into_file(file, only_decision=True, max_num_states=self.size()) + return self._model()._into_file(file, only_decision=True, max_num_states=self.size()) - cdef Model _model(self): + cdef _GraphManager _model(self): """Get a ref-counted Model object.""" - cdef Model m = self._model_ref() + cdef _GraphManager m = self._model_ref() if m is None: raise ReferenceError("accessing the states of a garbage collected model") return m @@ -1295,7 +1008,7 @@ cdef class States: def to_file(self): """Serialize the states to a new file-like object.""" self.resolve() - return self._model().to_file(only_decision=True, max_num_states=self.size()) + return self._model()._to_file(only_decision=True, max_num_states=self.size()) cdef class Symbol: @@ -1318,7 +1031,7 @@ cdef class Symbol: cls = type(self) return f"<{cls.__module__}.{cls.__qualname__} at {self.id():#x}>" - cdef void initialize_node(self, Model model, cppNode* node_ptr) noexcept: + cdef void initialize_node(self, _GraphManager model, cppNode* node_ptr) noexcept: self.model = model self.node_ptr = node_ptr @@ -1349,7 +1062,7 @@ cdef class Symbol: return deref(self.expired_ptr) @staticmethod - cdef Symbol from_ptr(Model model, cppNode* ptr): + cdef Symbol from_ptr(_GraphManager model, cppNode* ptr): """Construct a Symbol from a C++ Node pointer. There are times when a Node* needs to be passed through the Python layer @@ -1370,7 +1083,7 @@ cdef class Symbol: raise ValueError("Symbols cannot be constructed directly") @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): """Construct a symbol from a compressed file. Args: @@ -1401,27 +1114,27 @@ cdef class Symbol: Returns: True if the state is initialized. """ - if not self.model.is_locked() and self.node_ptr.topological_index() < 0: + if not self.model._is_locked() and self.node_ptr.topological_index() < 0: raise TypeError("the state of an intermediate variable cannot be accessed without " "locking the model first. See model.lock().") - cdef Py_ssize_t num_states = self.model.states.size() + cdef Py_ssize_t num_states = self.model._states.size() if not -num_states <= index < num_states: raise ValueError(f"index out of range: {index}") if index < 0: # allow negative indexing index += num_states - self.model.states.resolve() + self.model._states.resolve() # States are extended lazily, so if the state isn't yet long enough then this # node's state has not been initialized - if (self.model.states._states[index].size()) <= self.node_ptr.topological_index(): + if (self.model._states._states[index].size()) <= self.node_ptr.topological_index(): return False # Check that the state pointer is not null # We need to explicitly cast to evoke unique_ptr's operator bool - return (self.model.states._states[index][self.node_ptr.topological_index()]) + return (self.model._states._states[index][self.node_ptr.topological_index()]) cpdef uintptr_t id(self) noexcept: """Return the "identity" of the underlying node. @@ -1615,20 +1328,20 @@ cdef class Symbol: state 0: [0. 1. 2. 3. 4.] and [] state 1: [3. 4.] and [0. 1. 2.] """ - if not 0 <= index < self.model.states.size(): + if not 0 <= index < self.model._states.size(): raise ValueError(f"index out of range: {index}") if self.node_ptr.topological_index() < 0: # unsorted nodes don't have a state to reset return - self.model.states.resolve() + self.model._states.resolve() # make sure the state vector at least contains self - if (self.model.states._states[index].size()) <= self.node_ptr.topological_index(): - self.model.states._states[index].resize(self.node_ptr.topological_index() + 1) + if (self.model._states._states[index].size()) <= self.node_ptr.topological_index(): + self.model._states._states[index].resize(self.node_ptr.topological_index() + 1) - self.model._graph.recursive_reset(self.model.states._states[index], self.node_ptr) + self.model._graph.recursive_reset(self.model._states._states[index], self.node_ptr) def shares_memory(self, other): """Determine if two symbols share memory. @@ -1750,7 +1463,7 @@ cdef class ArraySymbol(Symbol): # via their subclasses. raise ValueError("ArraySymbols cannot be constructed directly") - cdef void initialize_arraynode(self, Model model, cppArrayNode* array_ptr) noexcept: + cdef void initialize_arraynode(self, _GraphManager model, cppArrayNode* array_ptr) noexcept: self.array_ptr = array_ptr self.initialize_node(model, array_ptr) @@ -2057,14 +1770,14 @@ cdef class ArraySymbol(Symbol): # todo: document once implemented raise NotImplementedError("copy=False is not (yet) supported") - cdef Py_ssize_t num_states = self.model.states.size() + cdef Py_ssize_t num_states = self.model._states.size() if not -num_states <= index < num_states: raise ValueError(f"index out of range: {index}") elif index < 0: # allow negative indexing index += num_states - if not self.model.is_locked() and self.node_ptr.topological_index() < 0: + if not self.model._is_locked() and self.node_ptr.topological_index() < 0: raise TypeError("the state of an intermediate variable cannot be accessed without " "locking the model first. See model.lock().") @@ -2175,28 +1888,28 @@ cdef class StateView: # we're assuming this object is being created because we want to access # the state, so let's go ahead and create the state if it's not already # there - symbol.model.states.resolve() - symbol.model._graph.recursive_initialize(symbol.model.states._states[index], symbol.node_ptr) + symbol.model._states.resolve() + symbol.model._graph.recursive_initialize(symbol.model._states._states[index], symbol.node_ptr) def __getbuffer__(self, Py_buffer *buffer, int flags): # todo: inspect/respect/test flags - self.symbol.model.states.resolve() + self.symbol.model._states.resolve() cdef cppArray* ptr = self.symbol.array_ptr - buffer.buf = (ptr.buff(self.symbol.model.states._states.at(self.index))) + buffer.buf = (ptr.buff(self.symbol.model._states._states.at(self.index))) buffer.format = (ptr.format().c_str()) buffer.internal = NULL buffer.itemsize = ptr.itemsize() - buffer.len = ptr.len(self.symbol.model.states._states.at(self.index)) + buffer.len = ptr.len(self.symbol.model._states._states.at(self.index)) buffer.ndim = ptr.ndim() buffer.obj = self buffer.readonly = 1 # todo: consider loosening this requirement - buffer.shape = (ptr.shape(self.symbol.model.states._states.at(self.index)).data()) + buffer.shape = (ptr.shape(self.symbol.model._states._states.at(self.index)).data()) buffer.strides = (ptr.strides().data()) buffer.suboffsets = NULL - self.symbol.model.states._view_count += 1 + self.symbol.model._states._view_count += 1 def __releasebuffer__(self, Py_buffer *buffer): - self.symbol.model.states._view_count -= 1 + self.symbol.model._states._view_count -= 1 diff --git a/dwave/optimization/model.py b/dwave/optimization/model.py new file mode 100644 index 00000000..afbf91e6 --- /dev/null +++ b/dwave/optimization/model.py @@ -0,0 +1,736 @@ +from typing import Optional +from dwave.optimization.graph_manager import ( + _GraphManager, + ArraySymbol, +) + +__all__ = ["Model"] + + +class Model(_GraphManager): + """Nonlinear model. + + The nonlinear model represents a general optimization problem with an + :term:`objective function` and/or constraints over variables of various + types. + + The :class:`.Model` class can contain this model and its methods provide + convenient utilities for working with representations of a problem. + + Examples: + This example creates a model for a + :class:`flow-shop-scheduling ` + problem with two jobs on three machines. + + >>> from dwave.optimization.generators import flow_shop_scheduling + ... + >>> processing_times = [[10, 5, 7], [20, 10, 15]] + >>> model = flow_shop_scheduling(processing_times=processing_times) + """ + + def __init__(self): + super().__init__() + + def add_constraint(self, value: ArraySymbol): + """Add a constraint to the model. + + Args: + value: Value that must evaluate to True for the state + of the model to be feasible. + + Returns: + The constraint symbol. + + Examples: + This example adds a single constraint to a model. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> i = model.integer() + >>> c = model.constant(5) + >>> constraint_sym = model.add_constraint(i <= c) + + The returned constraint symbol can be assigned and evaluated + for a model state: + + >>> with model.lock(): + ... model.states.resize(1) + ... i.set_state(0, 1) # Feasible state + ... print(constraint_sym.state(0)) + 1.0 + >>> with model.lock(): + ... i.set_state(0, 6) # Infeasible state + ... print(constraint_sym.state(0)) + 0.0 + """ + return self._add_constraint(value) + + def binary(self, shape=None): + r"""Create a binary symbol as a decision variable. + + Args: + shape: Shape of the binary array to create. + + Returns: + A binary symbol. + + Examples: + This example creates a :math:`1 \times 20`-sized binary symbol. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> x = model.binary((1,20)) + """ + from dwave.optimization.symbols import BinaryVariable # avoid circular import + return BinaryVariable(self, shape) + + def constant(self, array_like): + r"""Create a constant symbol. + + Args: + array_like: An |array-like|_ representing a constant. Can be a scalar + or a NumPy array. If the array's ``dtype`` is ``np.double``, the + array is not copied. + + Returns: + A constant symbol. + + Examples: + This example creates a :math:`1 \times 4`-sized constant symbol + with the specified values. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> time_limits = model.constant([10, 15, 5, 8.5]) + """ + return self._constant(array_like) + + def decision_state_size(self): + r"""An estimated size, in bytes, of the model's decision states. + + Examples: + This example checks the size of a model with one + :math:`10 \times 10`-sized integer symbol. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> visit_site = model.integer((10, 10)) + >>> model.decision_state_size() + 800 + """ + return self._decision_state_size() + + def disjoint_bit_sets(self, primary_set_size: int, num_disjoint_sets: int): + """Create a disjoint-sets symbol as a decision variable. + + Divides a set of the elements of ``range(primary_set_size)`` into + ``num_disjoint_sets`` ordered partitions, stored as bit sets (arrays + of length ``primary_set_size``, with ones at the indices of elements + currently in the set, and zeros elsewhere). The ordering of a set is + not semantically meaningful. + + Also creates from the symbol ``num_disjoint_sets`` extra successors + that output the disjoint sets as arrays. + + Args: + primary_set_size: Number of elements in the primary set that are + partitioned into disjoint sets. Must be non-negative. + num_disjoint_sets: Number of disjoint sets. Must be positive. + + Returns: + A tuple where the first element is the disjoint-sets symbol and + the second is a set of its newly added successors. + + Examples: + This example creates a symbol of 10 elements that is divided + into 4 sets. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> parts_set, parts_subsets = model.disjoint_bit_sets(10, 4) + """ + + # avoid circular import + from dwave.optimization.symbols import DisjointBitSets, DisjointBitSet + + main: DisjointBitSets = DisjointBitSets(self, primary_set_size, num_disjoint_sets) + sets = tuple(DisjointBitSet(main, i) for i in range(num_disjoint_sets)) + return main, sets + + def disjoint_lists(self, primary_set_size: int, num_disjoint_lists: int): + """Create a disjoint-lists symbol as a decision variable. + + Divides a set of the elements of ``range(primary_set_size)`` into + ``num_disjoint_lists`` ordered partitions. + + Also creates ``num_disjoint_lists`` extra successors from the + symbol that output the disjoint lists as arrays. + + Args: + primary_set_size: Number of elements in the primary set to + be partitioned into disjoint lists. + num_disjoint_lists: Number of disjoint lists. + + Returns: + A tuple where the first element is the disjoint-lists symbol + and the second is a list of its newly added successor nodes. + + Examples: + This example creates a symbol of 10 elements that is divided + into 4 lists. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> destinations, routes = model.disjoint_lists(10, 4) + """ + from dwave.optimization.symbols import DisjointLists, DisjointList # avoid circular import + main: DisjointLists = DisjointLists(self, primary_set_size, num_disjoint_lists) + lists = [DisjointList(main, i) for i in range(num_disjoint_lists)] + return main, lists + + def feasible(self, index: int = 0): + """Check the feasibility of the state at the input index. + + Args: + index: index of the state to check for feasibility. + + Returns: + Feasibility of the state. + + Examples: + This example demonstrates checking the feasibility of a simple model with + feasible and infeasible states. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> b = model.binary() + >>> model.add_constraint(b) # doctest: +ELLIPSIS + + >>> model.states.resize(2) + >>> b.set_state(0, 1) # Feasible + >>> b.set_state(1, 0) # Infeasible + >>> with model.lock(): + ... model.feasible(0) + True + >>> with model.lock(): + ... model.feasible(1) + False + """ + return all(sym.state(index) for sym in self.iter_constraints()) + + @classmethod + def from_file(cls, file, *, check_header=True): + """Construct a model from the given file. + + Args: + file: + File pointer to a readable, seekable file-like object encoding + a model. Strings are interpreted as a file name. + + Returns: + A model. + + See also: + :meth:`.into_file`, :meth:`.to_file` + """ + return cls._from_file(file, check_header=check_header) + + def integer(self, shape=None, lower_bound=None, upper_bound=None): + r"""Create an integer symbol as a decision variable. + + Args: + shape: Shape of the integer array to create. + + lower_bound: Lower bound for the symbol, which is the + smallest allowed integer value. If None, the default + value is used. + upper_bound: Upper bound for the symbol, which is the + largest allowed integer value. If None, the default + value is used. + + Returns: + An integer symbol. + + Examples: + This example creates a :math:`20 \times 20`-sized integer symbol. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> i = model.integer((20,20), lower_bound=-100, upper_bound=100) + """ + from dwave.optimization.symbols import IntegerVariable # avoid circular import + return IntegerVariable(self, shape, lower_bound, upper_bound) + + def into_file(self, file, *, max_num_states: int = 0, only_decision: bool = False): + """Serialize the model into an existing file. + + Args: + file: + File pointer to an existing writeable, seekable + file-like object encoding a model. Strings are + interpreted as a file name. + max_num_states: + Maximum number of states to serialize along with the model. + The number of states serialized is + ``min(model.states.size(), max_num_states)``. + only_decision: + If ``True``, only decision variables are serialized. + If ``False``, all symbols are serialized. + + See also: + :meth:`.from_file`, :meth:`.to_file` + + TODO: describe the format + """ + return self._into_file(file, max_num_states=max_num_states, only_decision=only_decision) + + def is_locked(self): + """Lock status of the model. + + No new symbols can be added to a locked model. + + See also: + :meth:`.lock`, :meth:`.unlock` + """ + return self._is_locked() + + def iter_constraints(self): + """Iterate over all constraints in the model. + + Examples: + This example adds a single constraint to a model and iterates over it. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> i = model.integer() + >>> c = model.constant(5) + >>> model.add_constraint(i <= c) # doctest: +ELLIPSIS + + >>> constraints = next(model.iter_constraints()) + """ + return self._iter_constraints() + + def iter_decisions(self): + """Iterate over all decision variables in the model. + + Examples: + This example adds a single decision symbol to a model and iterates over it. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> i = model.integer() + >>> c = model.constant(5) + >>> model.add_constraint(i <= c) # doctest: +ELLIPSIS + + >>> decisions = next(model.iter_decisions()) + """ + return self._iter_decisions() + + def iter_symbols(self): + """Iterate over all symbols in the model. + + Examples: + This example iterates over a model's symbols. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> i = model.integer(1, lower_bound=10) + >>> c = model.constant([[2, 3], [5, 6]]) + >>> symbol_1, symbol_2 = model.iter_symbols() + """ + return self._iter_symbols() + + def list(self, n: int): + """Create a list symbol as a decision variable. + + Args: + n: Values in the list are permutations of ``range(n)``. + + Returns: + A list symbol. + + Examples: + This example creates a list symbol of 200 elements. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> routes = model.list(200) + """ + from dwave.optimization.symbols import ListVariable # avoid circular import + return ListVariable(self, n) + + def lock(self): + """Lock the model. + + No new symbols can be added to a locked model. + + Returns: + A context manager. If the context is subsequently exited then the + :meth:`.unlock` will be called. + + See also: + :meth:`.is_locked`, :meth:`.unlock` + + Examples: + This example checks the status of a model after locking it and + subsequently unlocking it. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> i = model.integer(20, upper_bound=100) + >>> cntx = model.lock() + >>> model.is_locked() + True + >>> model.unlock() + >>> model.is_locked() + False + + This example locks a model temporarily with a context manager. + + >>> model = Model() + >>> with model.lock(): + ... # no nodes can be added within the context + ... print(model.is_locked()) + True + >>> model.is_locked() + False + """ + + return self._lock() + + def minimize(self, value: ArraySymbol): + """Set the objective value to minimize. + + Optimization problems have an objective and/or constraints. The objective + expresses one or more aspects of the problem that should be minimized + (equivalent to maximization when multiplied by a minus sign). For example, + an optimized itinerary might minimize the value of distance traveled or + cost of transportation or travel time. + + Args: + value: Value for which to minimize the cost function. + + Examples: + This example minimizes a simple polynomial, :math:`y = i^2 - 4i`, + within bounds. + + >>> from dwave.optimization import Model + >>> model = Model() + >>> i = model.integer(lower_bound=-5, upper_bound=5) + >>> c = model.constant(4) + >>> y = i*i - c*i + >>> model.minimize(y) + """ + self._minimize(value) + + def num_constraints(self): + """Number of constraints in the model. + + Examples: + This example checks the number of constraints in the model after + adding a couple of constraints. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> i = model.integer() + >>> c = model.constant([5, -14]) + >>> model.add_constraint(i <= c[0]) # doctest: +ELLIPSIS + + >>> model.add_constraint(c[1] <= i) # doctest: +ELLIPSIS + + >>> model.num_constraints() + 2 + """ + return self._num_constraints() + + def num_decisions(self): + """Number of independent decision nodes in the model. + + An array-of-integers symbol, for example, counts as a single + decision node. + + Examples: + This example checks the number of decisions in a model after + adding a single (size 20) decision symbol. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> c = model.constant([1, 5, 8.4]) + >>> i = model.integer(20, upper_bound=100) + >>> model.num_decisions() + 1 + """ + return self._num_decisions() + + def num_edges(self): + """Number of edges in the directed acyclic graph for the model. + + Examples: + This example minimizes the sum of a single constant symbol and + a single decision symbol, then checks the number of edges in + the model. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> c = model.constant(5) + >>> i = model.integer() + >>> model.minimize(c + i) + >>> model.num_edges() + 2 + """ + return self._num_edges() + + def num_nodes(self): + """Number of nodes in the directed acyclic graph for the model. + + See also: + :meth:`.num_symbols` + + Examples: + This example add a single (size 20) decision symbol and + a single (size 3) constant symbol checks the number of + nodes in the model. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> c = model.constant([1, 5, 8.4]) + >>> i = model.integer(20, upper_bound=100) + >>> model.num_nodes() + 2 + """ + return self._num_nodes() + + def num_symbols(self): + """Number of symbols tracked by the model. + + Equivalent to the number of nodes in the directed acyclic + graph for the model. + + See also: + :meth:`.num_nodes` + + Examples: + This example add a single (size 20) decision symbol and + a single (size 3) constant symbol checks the number of + symbols in the model. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> c = model.constant([1, 5, 8.4]) + >>> i = model.integer(20, upper_bound=100) + >>> model.num_symbols() + 2 + """ + return self._num_symbols() + + @property + def objective(self): + """Objective to be minimized. + + Examples: + This example prints the value of the objective of a model representing + the simple polynomial, :math:`y = i^2 - 4i`, for a state with value + :math:`i=2.0`. + + >>> from dwave.optimization import Model + ... + >>> model = Model() + >>> i = model.integer(lower_bound=-5, upper_bound=5) + >>> c = model.constant(4) + >>> y = i**2 - c*i + >>> model.minimize(y) + >>> with model.lock(): + ... model.states.resize(1) + ... i.set_state(0, 2.0) + ... print(f"Objective = {model.objective.state(0)}") + Objective = -4.0 + """ + return self._objective + + def quadratic_model(self, x: ArraySymbol, quadratic, linear=None): + """Create a quadratic model from an array and a quadratic model. + + Args: + x: An array. + + quadratic: Quadratic values for the quadratic model. + + linear: Linear values for the quadratic model. + + Returns: + A quadratic model. + + Examples: + This example creates a quadratic model. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> x = model.binary(3) + >>> Q = {(0, 0): 0, (0, 1): 1, (0, 2): 2, (1, 1): 1, (1, 2): 3, (2, 2): 2} + >>> qm = model.quadratic_model(x, Q) + + """ + from dwave.optimization.symbols import QuadraticModel + return QuadraticModel(x, quadratic, linear) + + def remove_unused_symbols(self): + """Remove unused symbols from the model. + + A symbol is considered unused if all of the following are true : + + * It is not a decision. + * It is not an ancestor of the objective. + * It is not an ancestor of a constraint. + * It has no :class:`ArraySymbol` object(s) referring to it. See examples + below. + + Returns: + The number of symbols removed. + + Examples: + In this example we create a mix of unused and used symbols. Then + the unused symbols are removed with ``remove_unused_symbols()``. + + >>> from dwave.optimization import Model + >>> model = Model() + >>> x = model.binary(5) + >>> x.sum() # create a symbol that will never be used # doctest: +ELLIPSIS + + >>> model.minimize(x.prod()) + >>> model.num_symbols() + 3 + >>> model.remove_unused_symbols() + 1 + >>> model.num_symbols() + 2 + + In this example we create a mix of unused and used symbols. + However, unlike the previous example, we assign the unused symbol + to a name in the namespace. This prevents the symbol from being + removed. + + >>> from dwave.optimization import Model + >>> model = Model() + >>> x = model.binary(5) + >>> y = x.sum() # create a symbol and assign it a name + >>> model.minimize(x.prod()) + >>> model.num_symbols() + 3 + >>> model.remove_unused_symbols() + 0 + >>> model.num_symbols() + 3 + + """ + return self._remove_unused_symbols() + + def set(self, n: int, min_size: int = 0, max_size: Optional[int] = None): + """Create a set symbol as a decision variable. + + Args: + n: Values in the set are subsets of ``range(n)``. + min_size: Minimum set size. Defaults to ``0``. + max_size: Maximum set size. Defaults to ``n``. + + Returns: + A set symbol. + + Examples: + This example creates a set symbol of up to 4 elements + with values between 0 to 99. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> destinations = model.set(100, max_size=4) + """ + from dwave.optimization.symbols import SetVariable # avoid circular import + return SetVariable(self, n, min_size, n if max_size is None else max_size) + + @property + def states(self): + """States of the model. + + :ref:`States ` represent assignments of values + to a symbol. + + See also: + :ref:`States methods ` such as + :meth:`~dwave.optimization.model.States.size` and + :meth:`~dwave.optimization.model.States.resize`. + """ + return self._states + + def state_size(self): + """An estimate of the size, in bytes, of all states in the model. + + Iterates over the model's states and totals the sizes of all. + + Examples: + This example estimates the size of a model's states. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> c = model.constant([1, 5, 8.4]) + >>> i = model.integer(20, upper_bound=100) + >>> model.state_size() + 184 + """ + return self._state_size() + + def to_file(self, **kwargs): + """Serialize the model to a new file-like object. + + See also: + :meth:`.into_file`, :meth:`.from_file` + """ + return self._to_file(**kwargs) + + def to_networkx(self): + """Convert the model to a NetworkX graph. + + Returns: + A :obj:`NetworkX ` graph. + + Examples: + This example converts a model to a graph. + + >>> from dwave.optimization.model import Model + >>> model = Model() + >>> one = model.constant(1) + >>> two = model.constant(2) + >>> i = model.integer() + >>> model.minimize(two * i - one) + >>> G = model.to_networkx() # doctest: +SKIP + + One advantage of converting to NetworkX is the wide availability + of drawing tools. See NetworkX's + `drawing `_ + documentation. + + This example uses `DAGVIZ `_ to + draw the NetworkX graph created in the example above. + + >>> import dagviz # doctest: +SKIP + >>> r = dagviz.render_svg(G) # doctest: +SKIP + >>> with open("model.svg", "w") as f: # doctest: +SKIP + ... f.write(r) + + This creates the following image: + + .. figure:: /_images/to_networkx_example.svg + :width: 500 px + :name: dwave-optimization-to-networkx-example + :alt: Image of NetworkX Directed Graph + + """ + return self._to_networkx() + + def unlock(self): + """Release a lock, decrementing the lock count. + + Symbols can be added to unlocked models only. + + See also: + :meth:`.is_locked`, :meth:`.lock` + """ + return self._unlock() diff --git a/dwave/optimization/model.pyi b/dwave/optimization/model.pyi deleted file mode 100644 index 27197ffd..00000000 --- a/dwave/optimization/model.pyi +++ /dev/null @@ -1,190 +0,0 @@ -# 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 collections.abc -import contextlib -import tempfile -import typing - -import numpy -import numpy.typing - -from dwave.optimization.symbols import * - - -_ShapeLike: typing.TypeAlias = typing.Union[int, collections.abc.Sequence[int]] - - -class Model: - def __init__(self): ... - - @property - def objective(self) -> ArraySymbol: ... - @property - def states(self) -> States: ... - - def add_constraint(self, value: ArraySymbol) -> ArraySymbol: ... - def binary(self, shape: typing.Optional[_ShapeLike] = None) -> BinaryVariable: ... - def constant(self, array_like: numpy.typing.ArrayLike) -> Constant: ... - def decision_state_size(self) -> int: ... - - def disjoint_bit_sets( - self, primary_set_size: int, num_disjoint_sets: int, - ) -> tuple[DisjointBitSets, tuple[DisjointBitSet, ...]]: ... - - def disjoint_lists( - self, primary_set_size: int, num_disjoint_lists: int, - ) -> tuple[DisjointLists, tuple[DisjointList, ...]]: ... - - def feasible(self, index: int = 0) -> bool: ... - - @classmethod - def from_file( - cls, - file: typing.Union[typing.BinaryIO, collections.abc.ByteString, str], - *, - check_header: bool = True, - ) -> Model: ... - - def integer( - self, - shape: typing.Optional[_ShapeLike] = None, - lower_bound: typing.Optional[int] = None, - upper_bound: typing.Optional[int] = None, - ) -> IntegerVariable: ... - - def into_file( - self, - file: typing.Union[typing.BinaryIO, collections.abc.ByteString, str], - *, - max_num_states: int = 0, - only_decision: bool = False, - ): ... - - def is_locked(self) -> bool: ... - def iter_constraints(self) -> collections.abc.Iterator[ArraySymbol]: ... - def iter_decisions(self) -> collections.abc.Iterator[Symbol]: ... - def iter_symbols(self) -> collections.abc.Iterator[Symbol]: ... - def list(self, n: int) -> ListVariable: ... - def lock(self) -> contextlib.AbstractContextManager: ... - def minimize(self, value: ArraySymbol): ... - def num_constraints(self) -> int: ... - def num_decisions(self) -> int: ... - def num_nodes(self) -> int: ... - def num_symbols(self) -> int: ... - - # dev note: this is underspecified, but it would be quite complex to fully - # specify the linear/quadratic so let's leave it alone for now. - def quadratic_model(self, x: ArraySymbol, quadratic, linear=None) -> QuadraticModel: ... - - def remove_unused_symbols(self) -> int: ... - - def set( - self, - n: int, - min_size: int = 0, - max_size: typing.Optional[int] = None, - ) -> SetVariable: ... - - def state_size(self) -> int: ... - - def to_file( - self, - *, - max_num_states: int = 0, - only_decision: bool = False, - ) -> typing.BinaryIO: ... - - # networkx might not be installed, so we just say we return an object. - def to_networkx(self) -> object: ... - - def unlock(self): ... - - -class States: - def __init__(self, model: Model): ... - def __len__(self) -> int: ... - def clear(self): ... - - def from_file( - self, - file: typing.Union[typing.BinaryIO, collections.abc.ByteString, str], - *, - replace: bool = True, - check_header: bool = True, - ) -> Model: ... - - def from_future(self, future: object, result_hook: collections.abc.Callable): ... - def initialize(self): ... - - def into_file( - self, - file: typing.Union[typing.BinaryIO, collections.abc.ByteString, str], - ): ... - - def resize(self, n: int): ... - def resolve(self): ... - def size(self) -> int: ... - def to_file(self) -> typing.BinaryIO: ... - - -class Symbol: - def __init__(self, *args, **kwargs) -> typing.NoReturn: ... - def equals(self, other: Symbol) -> bool: ... - def expired(self) -> bool: ... - def has_state(self, index: int = 0) -> bool: ... - def id(self) -> int: ... - def iter_predecessors(self) -> collections.abc.Iterator[Symbol]: ... - def iter_successors(self) -> collections.abc.Iterator[Symbol]: ... - def maybe_equals(self, other: Symbol) -> int: ... - def reset_state(self, index: int): ... - def shares_memory(self, other: Symbol) -> bool: ... - def state_size(self) -> int: ... - def topological_index(self) -> int: ... - - -class ArraySymbol(Symbol): - def __init__(self, *args, **kwargs) -> typing.NoReturn: ... - def __abs__(self) -> Absolute: ... - def __add__(self, rhs: ArraySymbol) -> Add: ... - def __bool__(self) -> typing.NoReturn: ... - def __eq__(self, rhs: ArraySymbol) -> Equal: ... - - def __getitem__( - self, - index: typing.Union[Symbol, int, slice, tuple], - ) -> typing.Union[AdvancedIndexing, BasicIndexing, Permutation]: ... - - def __iadd__(self, rhs: ArraySymbol) -> NaryAdd: ... - def __imul__(self, rhs: ArraySymbol) -> NaryMultiply: ... - def __le__(self, rhs: ArraySymbol) -> LessEqual: ... - def __mod__(self, rhs: ArraySymbol) -> Modulus: ... - def __mul__(self, rhs: ArraySymbol) -> Multiply: ... - def __neg__(self) -> Negative: ... - def __pow__(self, exponent: int) -> ArraySymbol: ... - def __sub__(self, rhs: ArraySymbol) -> Subtract: ... - def all(self) -> All: ... - def any(self) -> Any: ... - def max(self) -> Max: ... - def min(self) -> Min: ... - def ndim(self) -> int: ... - def prod(self) -> Prod: ... - def reshape(self, shape: _ShapeLike) -> Reshape: ... - def shape(self) -> tuple[int, ...]: ... - def size(self) -> typing.Union[int, Size]: ... - def sqrt(self) -> ArraySymbol: ... - def state(self, index: int = 0, *, copy: bool = True) -> numpy.ndarray: ... - def state_size(self) -> int: ... - def strides(self) -> tuple[int, ...]: ... - def sum(self, axis: typing.Optional[int] = None) -> typing.Union[Sum, PartialSum]: ... diff --git a/dwave/optimization/symbols.pxd b/dwave/optimization/symbols.pxd index 88cb00eb..985d6735 100644 --- a/dwave/optimization/symbols.pxd +++ b/dwave/optimization/symbols.pxd @@ -16,10 +16,10 @@ from libcpp.typeinfo cimport type_info -from dwave.optimization.model cimport Model +from dwave.optimization.graph_manager cimport _GraphManager from dwave.optimization.libcpp.graph cimport Array as cppArray from dwave.optimization.libcpp.graph cimport Node as cppNode cdef void _register(object cls, const type_info& typeinfo) -cdef object symbol_from_ptr(Model model, cppNode* ptr) +cdef object symbol_from_ptr(_GraphManager model, cppNode* ptr) diff --git a/dwave/optimization/symbols.pyi b/dwave/optimization/symbols.pyi index db7dceec..99266fda 100644 --- a/dwave/optimization/symbols.pyi +++ b/dwave/optimization/symbols.pyi @@ -16,7 +16,7 @@ import typing import numpy.typing -from dwave.optimization.model import Symbol, ArraySymbol +from dwave.optimization.graph_manager import Symbol, ArraySymbol class Absolute(ArraySymbol): diff --git a/dwave/optimization/symbols.pyx b/dwave/optimization/symbols.pyx index 4ceebff5..f5a3bf13 100644 --- a/dwave/optimization/symbols.pyx +++ b/dwave/optimization/symbols.pyx @@ -95,7 +95,7 @@ from dwave.optimization.libcpp.nodes cimport ( WhereNode as cppWhereNode, XorNode as cppXorNode, ) -from dwave.optimization.model cimport ArraySymbol, Model, Symbol +from dwave.optimization.graph_manager cimport ArraySymbol, _GraphManager, _GraphManager, Symbol __all__ = [ "Absolute", @@ -173,7 +173,8 @@ cdef void _register(object cls, const type_info& typeinfo): _cpp_type_to_python[type_index(typeinfo)] = (cls) -cdef object symbol_from_ptr(Model model, cppNode* node_ptr): +# TODO: should this use ExpressionOrModel? +cdef object symbol_from_ptr(_GraphManager model, cppNode* node_ptr): """Create a Python/Cython symbol from a C++ Node*.""" # If it's null, either after the cast of just as given, then we can't get a symbol from it @@ -233,7 +234,7 @@ cdef class Absolute(ArraySymbol): """ def __init__(self, ArraySymbol x): - cdef Model model = x.model + cdef _GraphManager model = x.model self.ptr = model._graph.emplace_node[cppAbsoluteNode](x.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -271,7 +272,7 @@ cdef class Add(ArraySymbol): if lhs.model is not rhs.model: raise ValueError("lhs and rhs do not share the same underlying model") - cdef Model model = lhs.model + cdef _GraphManager model = lhs.model self.ptr = model._graph.emplace_node[cppAddNode](lhs.array_ptr, rhs.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -305,7 +306,7 @@ cdef class All(ArraySymbol): """ def __init__(self, ArraySymbol array): - cdef Model model = array.model + cdef _GraphManager model = array.model self.ptr = model._graph.emplace_node[cppAllNode](array.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -334,7 +335,7 @@ cdef class And(ArraySymbol): if lhs.model is not rhs.model: raise ValueError("lhs and rhs do not share the same underlying model") - cdef Model model = lhs.model + cdef _GraphManager model = lhs.model self.ptr = model._graph.emplace_node[cppAndNode](lhs.array_ptr, rhs.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -376,7 +377,7 @@ cdef class Any(ArraySymbol): .. versionadded:: 0.4.1 """ def __init__(self, ArraySymbol array): - cdef Model model = array.model + cdef _GraphManager model = array.model self.ptr = model._graph.emplace_node[cppAnyNode](array.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -397,7 +398,7 @@ _register(Any, typeid(cppAnyNode)) cdef class _ArrayValidation(Symbol): def __init__(self, ArraySymbol array_node): - cdef Model model = array_node.model + cdef _GraphManager model = array_node.model self.ptr = model._graph.emplace_node[cppArrayValidationNode](array_node.array_ptr) @@ -434,7 +435,7 @@ cdef class AdvancedIndexing(ArraySymbol): """ def __init__(self, ArraySymbol array, *indices): - cdef Model model = array.model + cdef _GraphManager model = array.model cdef vector[cppAdvancedIndexingNode.array_or_slice] cppindices @@ -505,7 +506,7 @@ cdef class AdvancedIndexing(ArraySymbol): return sym @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): cdef cppNode* ptr indices = [] @@ -563,7 +564,7 @@ cdef class BasicIndexing(ArraySymbol): """ def __init__(self, ArraySymbol array, *indices): - cdef Model model = array.model + cdef _GraphManager model = array.model cdef vector[cppBasicIndexingNode.slice_or_int] cppindices for index in indices: @@ -604,7 +605,7 @@ cdef class BasicIndexing(ArraySymbol): return sym @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): if len(predecessors) != 1: raise ValueError(f"`BasicIndexing` should have exactly one predecessor") @@ -663,7 +664,7 @@ cdef class BinaryVariable(ArraySymbol): >>> type(x) """ - def __init__(self, Model model, shape=None): + def __init__(self, _GraphManager model, shape=None): # Get an observing pointer to the node cdef vector[Py_ssize_t] vshape = _as_cppshape(tuple() if shape is None else shape) @@ -683,7 +684,7 @@ cdef class BinaryVariable(ArraySymbol): return x @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): """Construct a binary symbol from a compressed file. Args: @@ -775,7 +776,7 @@ cdef class BinaryVariable(ArraySymbol): items.push_back(arr[i]) # The validity of the state is checked in C++ - self.ptr.initialize_state(self.model.states._states[index], move(items)) + self.ptr.initialize_state(self.model._states._states[index], move(items)) # An observing pointer to the C++ BinaryNode cdef cppBinaryNode* ptr @@ -802,7 +803,7 @@ cdef class Concatenate(ArraySymbol): if len(inputs) < 1: raise TypeError("must have at least one predecessor node") - cdef Model model = inputs[0].model + cdef _GraphManager model = inputs[0].model cdef vector[cppArrayNode*] cppinputs cdef ArraySymbol array @@ -828,7 +829,7 @@ cdef class Concatenate(ArraySymbol): return m @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): if len(predecessors) < 1: raise ValueError("Concatenate must have at least one predecessor") @@ -859,7 +860,7 @@ cdef class Constant(ArraySymbol): >>> type(a) """ - def __init__(self, Model model, array_like): + def __init__(self, _GraphManager model, array_like): # In the future we won't need to be contiguous, but we do need to be right now array = np.asarray(array_like, dtype=np.double, order="C") @@ -960,7 +961,7 @@ cdef class Constant(ArraySymbol): return constant @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): """Construct a constant symbol from a compressed file. Args: @@ -1057,7 +1058,7 @@ cdef class DisjointBitSets(Symbol): """ def __init__( - self, Model model, Py_ssize_t primary_set_size, Py_ssize_t num_disjoint_sets + self, _GraphManager model, Py_ssize_t primary_set_size, Py_ssize_t num_disjoint_sets ): # Get an observing pointer to the node self.ptr = model._graph.emplace_node[cppDisjointBitSetsNode]( @@ -1078,7 +1079,7 @@ cdef class DisjointBitSets(Symbol): return x @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): """Construct a disjoint-sets symbol from a compressed file. Args: @@ -1168,7 +1169,7 @@ cdef class DisjointBitSets(Symbol): sets[i].push_back(arr[i, j]) # The validity of the state is checked in C++ - self.ptr.initialize_state(self.model.states._states[index], move(sets)) + self.ptr.initialize_state(self.model._states._states[index], move(sets)) def _state_from_zipfile(self, zf, directory, Py_ssize_t state_index): arrays = [] @@ -1218,7 +1219,7 @@ cdef class DisjointBitSet(ArraySymbol): if set_index > (parent.ptr.successors().size()): raise ValueError("`DisjointBitSet`s must be created successively") - cdef Model model = parent.model + cdef _GraphManager model = parent.model if set_index == (parent.ptr.successors().size()): # The DisjointBitSet has not been added to the model yet, so add it self.ptr = model._graph.emplace_node[cppDisjointBitSetNode](parent.ptr) @@ -1242,7 +1243,7 @@ cdef class DisjointBitSet(ArraySymbol): return x @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): """Construct a disjoint-set symbol from a compressed file. Args: @@ -1314,7 +1315,7 @@ cdef class DisjointLists(Symbol): """ def __init__( - self, Model model, Py_ssize_t primary_set_size, Py_ssize_t num_disjoint_lists + self, _GraphManager model, Py_ssize_t primary_set_size, Py_ssize_t num_disjoint_lists ): # Get an observing pointer to the node self.ptr = model._graph.emplace_node[cppDisjointListsNode]( @@ -1334,7 +1335,7 @@ cdef class DisjointLists(Symbol): return x @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): """Construct a disjoint-lists symbol from a compressed file. Args: @@ -1421,7 +1422,7 @@ cdef class DisjointLists(Symbol): items[i].push_back(arr[j]) # The validity of the state is checked in C++ - self.ptr.initialize_state(self.model.states._states[index], move(items)) + self.ptr.initialize_state(self.model._states._states[index], move(items)) def _state_from_zipfile(self, zf, directory, Py_ssize_t state_index): arrays = [] @@ -1471,7 +1472,7 @@ cdef class DisjointList(ArraySymbol): if list_index > (parent.ptr.successors().size()): raise ValueError("`DisjointList`s must be created successively") - cdef Model model = parent.model + cdef _GraphManager model = parent.model if list_index == (parent.ptr.successors().size()): # The DisjointListNode has not been added to the model yet, so add it self.ptr = model._graph.emplace_node[cppDisjointListNode](parent.ptr) @@ -1495,7 +1496,7 @@ cdef class DisjointList(ArraySymbol): return x @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): """Construct a disjoint-list symbol from a compressed file. Args: @@ -1572,7 +1573,7 @@ cdef class Equal(ArraySymbol): if lhs.model is not rhs.model: raise ValueError("lhs and rhs do not share the same underlying model") - cdef Model model = lhs.model + cdef _GraphManager model = lhs.model self.ptr = model._graph.emplace_node[cppEqualNode](lhs.array_ptr, rhs.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -1604,7 +1605,7 @@ cdef class IntegerVariable(ArraySymbol): >>> type(i) """ - def __init__(self, Model model, shape=None, lower_bound=None, upper_bound=None): + def __init__(self, _GraphManager model, shape=None, lower_bound=None, upper_bound=None): cdef vector[Py_ssize_t] vshape = _as_cppshape(tuple() if shape is None else shape ) if lower_bound is None and upper_bound is None: @@ -1630,7 +1631,7 @@ cdef class IntegerVariable(ArraySymbol): return x @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): if predecessors: raise ValueError(f"{cls.__name__} cannot have predecessors") @@ -1682,7 +1683,7 @@ cdef class IntegerVariable(ArraySymbol): items.push_back(arr[i]) # The validity of the state is checked in C++ - self.ptr.initialize_state(self.model.states._states[index], move(items)) + self.ptr.initialize_state(self.model._states._states[index], move(items)) def upper_bound(self): """The highest value allowed for the integer symbol.""" @@ -1712,7 +1713,7 @@ cdef class LessEqual(ArraySymbol): if lhs.model is not rhs.model: raise ValueError("lhs and rhs do not share the same underlying model") - cdef Model model = lhs.model + cdef _GraphManager model = lhs.model self.ptr = model._graph.emplace_node[cppLessEqualNode](lhs.array_ptr, rhs.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -1744,7 +1745,7 @@ cdef class ListVariable(ArraySymbol): >>> type(l) """ - def __init__(self, Model model, Py_ssize_t n): + def __init__(self, _GraphManager model, Py_ssize_t n): # Get an observing pointer to the node self.ptr = model._graph.emplace_node[cppListNode](n) @@ -1762,7 +1763,7 @@ cdef class ListVariable(ArraySymbol): return x @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): if predecessors: raise ValueError(f"{cls.__name__} cannot have predecessors") @@ -1807,7 +1808,7 @@ cdef class ListVariable(ArraySymbol): items.push_back(arr[i]) # The validity of the state is checked in C++ - self.ptr.initialize_state(self.model.states._states[index], move(items)) + self.ptr.initialize_state(self.model._states._states[index], move(items)) # An observing pointer to the C++ ListNode cdef cppListNode* ptr @@ -1822,7 +1823,7 @@ cdef class Logical(ArraySymbol): :func:`~dwave.optimization.mathematical.logical`: equivalent function. """ def __init__(self, ArraySymbol x): - cdef Model model = x.model + cdef _GraphManager model = x.model self.ptr = model._graph.emplace_node[cppLogicalNode](x.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -1857,7 +1858,7 @@ cdef class Max(ArraySymbol): """ def __init__(self, ArraySymbol node): - cdef Model model = node.model + cdef _GraphManager model = node.model self.ptr = model._graph.emplace_node[cppMaxNode](node.array_ptr) @@ -1899,7 +1900,7 @@ cdef class Maximum(ArraySymbol): if lhs.model is not rhs.model: raise ValueError("lhs and rhs do not share the same underlying model") - cdef Model model = lhs.model + cdef _GraphManager model = lhs.model self.ptr = model._graph.emplace_node[cppMaximumNode](lhs.array_ptr, rhs.array_ptr) @@ -1935,7 +1936,7 @@ cdef class Min(ArraySymbol): """ def __init__(self, ArraySymbol node): - cdef Model model = node.model + cdef _GraphManager model = node.model self.ptr = model._graph.emplace_node[cppMinNode](node.array_ptr) @@ -1977,7 +1978,7 @@ cdef class Minimum(ArraySymbol): if lhs.model is not rhs.model: raise ValueError("lhs and rhs do not share the same underlying model") - cdef Model model = lhs.model + cdef _GraphManager model = lhs.model self.ptr = model._graph.emplace_node[cppMinimumNode](lhs.array_ptr, rhs.array_ptr) @@ -2016,7 +2017,7 @@ cdef class Modulus(ArraySymbol): if lhs.model is not rhs.model: raise ValueError("lhs and rhs do not share the same underlying model") - cdef Model model = lhs.model + cdef _GraphManager model = lhs.model self.ptr = model._graph.emplace_node[cppModulusNode](lhs.array_ptr, rhs.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -2054,7 +2055,7 @@ cdef class Multiply(ArraySymbol): if lhs.model is not rhs.model: raise ValueError("lhs and rhs do not share the same underlying model") - cdef Model model = lhs.model + cdef _GraphManager model = lhs.model self.ptr = model._graph.emplace_node[cppMultiplyNode](lhs.array_ptr, rhs.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -2095,7 +2096,7 @@ cdef class NaryAdd(ArraySymbol): if len(inputs) == 0: raise TypeError("must have at least one predecessor node") - cdef Model model = inputs[0].model + cdef _GraphManager model = inputs[0].model cdef vector[cppArrayNode*] cppinputs cdef ArraySymbol array @@ -2152,7 +2153,7 @@ cdef class NaryMaximum(ArraySymbol): if len(inputs) == 0: raise TypeError("must have at least one predecessor node") - cdef Model model = inputs[0].model + cdef _GraphManager model = inputs[0].model cdef vector[cppArrayNode*] cppinputs cdef ArraySymbol array @@ -2202,7 +2203,7 @@ cdef class NaryMinimum(ArraySymbol): if len(inputs) == 0: raise TypeError("must have at least one predecessor node") - cdef Model model = inputs[0].model + cdef _GraphManager model = inputs[0].model cdef vector[cppArrayNode*] cppinputs cdef ArraySymbol array @@ -2251,7 +2252,7 @@ cdef class NaryMultiply(ArraySymbol): if len(inputs) == 0: raise TypeError("must have at least one predecessor node") - cdef Model model = inputs[0].model + cdef _GraphManager model = inputs[0].model cdef vector[cppArrayNode*] cppinputs cdef ArraySymbol array @@ -2300,7 +2301,7 @@ cdef class Negative(ArraySymbol): """ def __init__(self, ArraySymbol x): - cdef Model model = x.model + cdef _GraphManager model = x.model self.ptr = model._graph.emplace_node[cppNegativeNode](x.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -2327,7 +2328,7 @@ cdef class Not(ArraySymbol): :func:`~dwave.optimization.mathematical.logical_not`: equivalent function. """ def __init__(self, ArraySymbol x): - cdef Model model = x.model + cdef _GraphManager model = x.model self.ptr = model._graph.emplace_node[cppNotNode](x.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -2357,7 +2358,7 @@ cdef class Or(ArraySymbol): if lhs.model is not rhs.model: raise ValueError("lhs and rhs do not share the same underlying model") - cdef Model model = lhs.model + cdef _GraphManager model = lhs.model self.ptr = model._graph.emplace_node[cppOrNode](lhs.array_ptr, rhs.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -2392,7 +2393,7 @@ cdef class PartialSum(ArraySymbol): """ def __init__(self, ArraySymbol array, int axis): - cdef Model model = array.model + cdef _GraphManager model = array.model self.ptr = model._graph.emplace_node[cppPartialSumNode](array.array_ptr, axis) self.initialize_arraynode(model, self.ptr) @@ -2411,7 +2412,7 @@ cdef class PartialSum(ArraySymbol): return ps @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): if len(predecessors) != 1: raise ValueError("PartialSum must have exactly one predecessor") @@ -2481,7 +2482,7 @@ cdef class Prod(ArraySymbol): """ def __init__(self, ArraySymbol node): - cdef Model model = node.model + cdef _GraphManager model = node.model self.ptr = model._graph.emplace_node[cppProdNode](node.array_ptr) @@ -2666,7 +2667,7 @@ cdef class QuadraticModel(ArraySymbol): return self.ptr.get_quadratic_model().get_quadratic(u, v) @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): """Construct a QuadraticModel from a zipfile.""" if len(predecessors) != 1: raise ValueError("Reshape must have exactly one predecessor") @@ -2747,7 +2748,7 @@ cdef class Reshape(ArraySymbol): """ def __init__(self, ArraySymbol node, shape): - cdef Model model = node.model + cdef _GraphManager model = node.model self.ptr = model._graph.emplace_node[cppReshapeNode]( node.array_ptr, @@ -2768,7 +2769,7 @@ cdef class Reshape(ArraySymbol): return m @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): if len(predecessors) != 1: raise ValueError("Reshape must have exactly one predecessor") @@ -2804,7 +2805,7 @@ cdef class SetVariable(ArraySymbol): >>> type(s) """ - def __init__(self, Model model, Py_ssize_t n, Py_ssize_t min_size, Py_ssize_t max_size): + def __init__(self, _GraphManager model, Py_ssize_t n, Py_ssize_t min_size, Py_ssize_t max_size): self.ptr = model._graph.emplace_node[cppSetNode](n, min_size, max_size) self.initialize_arraynode(model, self.ptr) @@ -2820,7 +2821,7 @@ cdef class SetVariable(ArraySymbol): return x @classmethod - def _from_zipfile(cls, zf, directory, Model model, predecessors): + def _from_zipfile(cls, zf, directory, _GraphManager model, predecessors): if predecessors: raise ValueError(f"{cls.__name__} cannot have predecessors") @@ -2873,7 +2874,7 @@ cdef class SetVariable(ArraySymbol): items.push_back(arr[i]) # The validity of the state is checked in C++ - self.ptr.initialize_state(self.model.states._states[index], move(items)) + self.ptr.initialize_state(self.model._states._states[index], move(items)) # Observing pointer to the node cdef cppSetNode* ptr @@ -2883,7 +2884,7 @@ _register(SetVariable, typeid(cppSetNode)) cdef class Size(ArraySymbol): def __init__(self, ArraySymbol array): - cdef Model model = array.model + cdef _GraphManager model = array.model self.ptr = model._graph.emplace_node[cppSizeNode](array.array_ptr) self.initialize_arraynode(array.model, self.ptr) @@ -2920,7 +2921,7 @@ cdef class Square(ArraySymbol): """ def __init__(self, ArraySymbol x): - cdef Model model = x.model + cdef _GraphManager model = x.model self.ptr = model._graph.emplace_node[cppSquareNode](x.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -2955,7 +2956,7 @@ cdef class SquareRoot(ArraySymbol): """ def __init__(self, ArraySymbol x): - cdef Model model = x.model + cdef _GraphManager model = x.model self.ptr = model._graph.emplace_node[cppSquareRootNode](x.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -2994,7 +2995,7 @@ cdef class Subtract(ArraySymbol): if lhs.model is not rhs.model: raise ValueError("lhs and rhs do not share the same underlying model") - cdef Model model = lhs.model + cdef _GraphManager model = lhs.model self.ptr = model._graph.emplace_node[cppSubtractNode](lhs.array_ptr, rhs.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -3029,7 +3030,7 @@ cdef class Sum(ArraySymbol): """ def __init__(self, ArraySymbol array): - cdef Model model = array.model + cdef _GraphManager model = array.model self.ptr = model._graph.emplace_node[cppSumNode](array.array_ptr) self.initialize_arraynode(model, self.ptr) @@ -3055,7 +3056,7 @@ cdef class Where(ArraySymbol): :func:`~dwave.optimization.mathematical.where`: equivalent function. """ def __init__(self, ArraySymbol condition, ArraySymbol x, ArraySymbol y): - cdef Model model = condition.model + cdef _GraphManager model = condition.model if condition.model is not x.model: raise ValueError("condition and x do not share the same underlying model") @@ -3093,7 +3094,7 @@ cdef class Xor(ArraySymbol): if lhs.model is not rhs.model: raise ValueError("lhs and rhs do not share the same underlying model") - cdef Model model = lhs.model + cdef _GraphManager model = lhs.model self.ptr = model._graph.emplace_node[cppXorNode](lhs.array_ptr, rhs.array_ptr) self.initialize_arraynode(model, self.ptr) diff --git a/meson.build b/meson.build index 97d295f8..5ef12098 100644 --- a/meson.build +++ b/meson.build @@ -65,8 +65,8 @@ else endif py.extension_module( - 'model', - 'dwave/optimization/model.pyx', + 'graph_manager', + 'dwave/optimization/graph_manager.pyx', dependencies: libdwave_optimization, gnu_symbol_visibility: 'default', install: true, diff --git a/tests/test_model.py b/tests/test_model.py index bfa8353e..864c46be 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -22,18 +22,19 @@ import numpy as np +import dwave.optimization.graph_manager import dwave.optimization.symbols from dwave.optimization import Model class TestArraySymbol(unittest.TestCase): def test_abstract(self): - from dwave.optimization.model import ArraySymbol + from dwave.optimization.graph_manager import ArraySymbol with self.assertRaisesRegex(ValueError, "ArraySymbols cannot be constructed directly"): ArraySymbol() def test_bool(self): - from dwave.optimization.model import ArraySymbol + from dwave.optimization.graph_manager import ArraySymbol # bypass the init, this should be done very carefully as it can lead to # segfaults dependig on what methods are accessed! @@ -107,7 +108,7 @@ class UnknownType(): with self.subTest("__pow__"): self.assertIsInstance(x ** 1, type(x)) - self.assertIsInstance(x ** 1, dwave.optimization.model.ArraySymbol) + self.assertIsInstance(x ** 1, dwave.optimization.graph_manager.ArraySymbol) self.assertIsInstance(x ** 2, dwave.optimization.symbols.Square) self.assertIsInstance(x ** 3, dwave.optimization.symbols.NaryMultiply) self.assertIsInstance(x ** 4, dwave.optimization.symbols.NaryMultiply) @@ -121,7 +122,7 @@ def __init__(self, array): def __getitem__(self, index): if not isinstance(index, tuple): return self[(index,)] - i0, i1 = dwave.optimization.model._split_indices(index) + i0, i1 = dwave.optimization.graph_manager._split_indices(index) np.testing.assert_array_equal(self.array[index], self.array[i0][i1]) def test_split_indices(self): @@ -607,7 +608,7 @@ def test_init(self): model = Model() self.assertTrue(hasattr(model, "states")) - self.assertIsInstance(model.states, dwave.optimization.model.States) + self.assertIsInstance(model.states, dwave.optimization.graph_manager.States) # by default there are no states self.assertEqual(len(model.states), 0) @@ -677,7 +678,7 @@ def test_serialization_bad(self): class TestSymbol(unittest.TestCase): def test_abstract(self): - from dwave.optimization.model import Symbol + from dwave.optimization.graph_manager import Symbol with self.assertRaisesRegex(ValueError, "Symbols cannot be constructed directly"): Symbol()