def _execute_single_test(problem, baseline, solver=None, comm=None, **kwds): if solver is None: solver = Solver(comm=comm) else: assert comm is None if solver.comm is not None: if solver.comm.rank == 0: pass elif solver.comm.rank == 1: pass elif solver.comm.rank == 3: pass orig = pybnb.node.Node() problem.save_state(orig) results = solver.solve(problem, **kwds) current = pybnb.node.Node() problem.save_state(current) assert len(current.state) == len(orig.state) for i in range(len(current.state)): assert current.state[i] == orig.state[i] assert len(vars(results)) > 0 assert len(vars(results)) == len(vars(baseline)) for name in sorted(sorted(list(vars(results).keys()))): if getattr(baseline, name) is _ignore_value_: continue if (name == 'nodes') and \ (solver.comm is not None) and \ (solver.comm.size > 2): pass else: assert getattr(results, name) == getattr(baseline, name), \ ("value for '"+str(name)+"' ("+ str(getattr(results, name))+") does " "not match baseline ("+ str(getattr(baseline, name))+")") if solver.is_dispatcher: q = solver.save_dispatcher_queue() assert len(q) == 2 assert q.next_tree_id >= 0 assert len(q.nodes) == solver._disp.queue.size()
class NestedSolver(_ProblemWithSolveInfoCollection): """A class for creating a nested branch-and-bound solve strategy. An instance of this class (wrapped around a standard problem) can be passed to the solver as the problem argument. Parameters ---------- problem : :class:`pybnb.Problem <pybnb.problem.Problem>` An object defining a branch-and-bound problem. node_limit : int, optional The same as the standard solver option, but applied to the nested solver to limit the number of nodes to explore when processing a work item. (default: None) time_limit : float, optional The same as the standard solver option, but applied to the nested solver to limit the amount of time spent processing a work item. (default: 5) queue_limit : int, optional The same as the standard solver option, but applied to the nested solver to limit the size of the queue. (default: None) track_bound : bool, optional The same as the standard solver option, but applied to the nested solver control bound tracking. (default: True) queue_strategy : :class:`QueueStrategy <pybnb.common.QueueStrategy>` or tuple The same as the standard solver option, but applied to the nested solver to control the queue strategy used when processing a work item. (default: 'depth') """ def __init__(self, problem, node_limit=None, time_limit=5, queue_limit=None, track_bound=True, queue_strategy="depth"): # we import from pybnb.solver here to avoid a # circular import reference from pybnb.solver import Solver self._problem = problem self._node_limit = node_limit self._time_limit = time_limit self._queue_limit = queue_limit self._track_bound = track_bound self._queue_strategy = queue_strategy self._solver = Solver(comm=None) self._log = logging.Logger(None, level=logging.WARNING) self._convergence_checker = None self._best_objective = None self._best_node = None self._current_node = None self._results = None self._queue = None super(NestedSolver, self).__init__() def _initialize(self, dispatcher, best_objective, disable_objective_call): assert best_objective is not None self._best_objective = best_objective self._disable_objective_call = disable_objective_call self._log.addHandler(_RedirectHandler(dispatcher)) def _solve(self): bound_stop = None if not math.isinf(self._best_objective): bound_stop = self._best_objective # shallow copy root = copy.copy(self._current_node) init_queue = DispatcherQueueData(nodes=[root], worst_terminal_bound=None, sense=self._convergence_checker.sense) self._results = self._solver.solve( self._problem, best_objective=self._best_objective, best_node=self._best_node, bound_stop=bound_stop, initialize_queue=init_queue, log=self._log, absolute_gap=\ self._convergence_checker.absolute_gap, relative_gap=\ self._convergence_checker.relative_gap, scale_function=\ self._convergence_checker.scale_function, queue_tolerance=\ self._convergence_checker.queue_tolerance, branch_tolerance=\ self._convergence_checker.branch_tolerance, comparison_tolerance=\ self._convergence_checker.comparison_tolerance, objective_stop=\ self._convergence_checker.objective_stop, disable_objective_call=self._disable_objective_call, node_limit=self._node_limit, time_limit=self._time_limit, queue_limit=self._queue_limit, track_bound=self._track_bound, queue_strategy=self._queue_strategy, disable_signal_handlers=True) self._queue = self._solver.save_dispatcher_queue() self._solve_info.add_from(self._solver._global_solve_info) # # Ducktype a partial Problem interface # def objective(self): #pragma:nocover """The solver does not call this method when it sees the problem implements a nested solve.""" raise NotImplementedError() def bound(self): #pragma:nocover """The solver does not call this method when it sees the problem implements a nested solve.""" raise NotImplementedError() def branch(self): #pragma:nocover """The solver does not call this method when it sees the problem implements a nested solve.""" raise NotImplementedError() def sense(self): """Calls the sense() method on the user-provided problem.""" return self._problem.sense() def save_state(self, node): """Calls the save_state() method on the user-provided problem.""" self._problem.save_state(node) def load_state(self, node): """Calls the load_state() method on the user-provided problem and prepares for a nested solve.""" self._problem.load_state(node) self._results = None self._children = None self._current_node = node self._solve_found_best_node = False def notify_solve_begins(self, comm, worker_comm, convergence_checker): """Calls the notify_solve_begins() method on the user-provided problem and prepares for a solve.""" self._best_objective = None self._best_node = None self._convergence_checker = convergence_checker self._current_node = None self._results = None self._queue = None self._problem.notify_solve_begins(comm, worker_comm, convergence_checker) def notify_new_best_node(self, node, current): """Calls the notify_new_best_node() method on the user-provided problem and stores the best node for use in the next nested solve.""" self._best_objective = self._convergence_checker.\ best_objective(self._best_objective, node.objective) self._best_node = node self._problem.notify_new_best_node(node, current) def notify_solve_finished(self, comm, worker_comm, results): """Calls the notify_solve_finished() method on the user-provided problem and does some final cleanup.""" while len(self._log.handlers) > 0: self._log.removeHandler(self._log.handlers[0]) self._problem.notify_solve_finished(comm, worker_comm, results)
def _test_initialize_queue(comm): solver = Solver(comm=comm) # no initial queue for sense in (minimize, maximize): problem = DummyProblem(sense) results = solver.solve(problem, queue_limit=0) assert results.solution_status == "unknown" assert results.termination_condition == "queue_limit" assert results.objective == (inf if (sense == minimize) else -inf) assert results.bound == (-inf if (sense == minimize) else inf) assert results.absolute_gap is None assert results.relative_gap is None assert results.nodes == 0 assert results.wall_time is not None assert results.best_node is None assert problem._notify_new_best_node_call_count == 0 assert solver._local_solve_info.explored_nodes_count == 0 problem = DummyProblem(sense) results = solver.solve(problem) assert results.solution_status == "optimal" assert results.termination_condition == "optimality" assert results.objective == 0 assert results.bound == 0 assert results.absolute_gap == 0 assert results.relative_gap == 0 assert results.nodes == 1 assert results.wall_time is not None assert results.best_node is not None assert results.best_node.objective == results.objective assert results.best_node.tree_depth == 0 if solver.is_worker: assert problem._notify_new_best_node_call_count == 1 assert problem._notify_new_best_node_args[0] is results.best_node if solver._local_solve_info.explored_nodes_count == 1: assert problem._notify_new_best_node_args[1] else: assert solver._local_solve_info.explored_nodes_count == 0 assert not problem._notify_new_best_node_args[1] else: assert problem._notify_new_best_node_call_count == 0 problem = DummyProblem(sense) results = solver.solve( problem, best_objective=(1 if (sense == minimize) else -1) ) assert results.solution_status == "optimal" assert results.termination_condition == "optimality" assert results.objective == 0 assert results.bound == 0 assert results.absolute_gap == 0 assert results.relative_gap == 0 assert results.nodes == 1 assert results.wall_time is not None assert results.best_node is not None assert results.best_node.objective == results.objective assert results.best_node.tree_depth == 0 if solver.is_worker: assert problem._notify_new_best_node_call_count == 1 assert problem._notify_new_best_node_args[0] is results.best_node if solver._local_solve_info.explored_nodes_count == 1: assert problem._notify_new_best_node_args[1] else: assert solver._local_solve_info.explored_nodes_count == 0 assert not problem._notify_new_best_node_args[1] else: assert problem._notify_new_best_node_call_count == 0 problem = DummyProblem(sense) results = solver.solve( problem, best_objective=(1 if (sense == minimize) else -1), disable_objective_call=True, ) assert results.solution_status == "feasible" assert results.termination_condition == "queue_empty" assert results.objective == (1 if (sense == minimize) else -1) assert results.bound == 0 assert results.absolute_gap == 1 assert results.relative_gap == 1 assert results.nodes == 1 assert results.wall_time is not None assert results.best_node is None assert problem._notify_new_best_node_call_count == 0 best_node_ = Node() best_node_.objective = 1 if (sense == minimize) else -1 best_node_._uuid = "abcd" problem = DummyProblem(sense) results = solver.solve( problem, best_node=best_node_, disable_objective_call=True ) assert results.solution_status == "feasible" assert results.termination_condition == "queue_empty" assert results.objective == best_node_.objective assert results.bound == 0 assert results.absolute_gap == 1 assert results.relative_gap == 1 assert results.nodes == 1 assert results.best_node._uuid == best_node_._uuid if (comm is None) or (comm.size == 1): assert results.best_node is best_node_ assert results.best_node.objective == results.objective if solver.is_worker: assert problem._notify_new_best_node_call_count == 1 assert problem._notify_new_best_node_args[0] is results.best_node if solver._local_solve_info.explored_nodes_count == 1: assert not problem._notify_new_best_node_args[1] else: assert solver._local_solve_info.explored_nodes_count == 0 assert not problem._notify_new_best_node_args[1] else: assert problem._notify_new_best_node_call_count == 0 best_node_ = Node() best_node_.objective = 1 if (sense == minimize) else -1 best_node_._uuid = "abcd" problem = DummyProblem(sense) results = solver.solve(problem, best_node=best_node_) assert results.solution_status == "optimal" assert results.termination_condition == "optimality" assert results.objective == 0 assert results.bound == 0 assert results.absolute_gap == 0 assert results.relative_gap == 0 assert results.nodes == 1 assert results.wall_time is not None assert results.best_node._uuid != best_node_._uuid if (comm is None) or (comm.size == 1): assert results.best_node is not best_node_ assert results.best_node.objective == results.objective assert results.best_node.tree_depth == 0 if solver.is_worker: assert problem._notify_new_best_node_call_count >= 1 assert problem._notify_new_best_node_args[0] is results.best_node if problem._notify_new_best_node_call_count == 2: assert problem._notify_new_best_node_args[1] else: assert problem._notify_new_best_node_args[0] else: assert problem._notify_new_best_node_call_count == 0 # empty initial queue queue = DispatcherQueueData(nodes=[], worst_terminal_bound=None, sense=minimize) for sense in (minimize, maximize): queue.sense = sense problem = DummyProblem(sense) results = solver.solve(problem, initialize_queue=queue) assert results.solution_status == "unknown" assert results.termination_condition == "queue_empty" assert results.objective == (inf if (sense == minimize) else -inf) assert results.bound == (-inf if (sense == minimize) else inf) assert results.absolute_gap is None assert results.relative_gap is None assert results.nodes == 0 assert results.wall_time is not None assert results.best_node is None assert problem._notify_new_best_node_call_count == 0 problem = DummyProblem(sense) results = solver.solve(problem, initialize_queue=queue, best_objective=0) assert results.solution_status == "feasible" assert results.termination_condition == "queue_empty" assert results.objective == 0 assert results.bound == (-inf if (sense == minimize) else inf) assert results.absolute_gap == inf assert results.relative_gap == inf assert results.nodes == 0 assert results.wall_time is not None assert results.best_node is None assert problem._notify_new_best_node_call_count == 0 problem = DummyProblem(sense) results = solver.solve( problem, initialize_queue=queue, best_objective=0, disable_objective_call=True, ) assert results.solution_status == "feasible" assert results.termination_condition == "queue_empty" assert results.objective == 0 assert results.bound == (-inf if (sense == minimize) else inf) assert results.absolute_gap == inf assert results.relative_gap == inf assert results.nodes == 0 assert results.wall_time is not None assert results.best_node is None assert problem._notify_new_best_node_call_count == 0 best_node_ = Node() best_node_.objective = 1 if (sense == minimize) else -1 best_node_._uuid = "abcd" problem = DummyProblem(sense) results = solver.solve( problem, initialize_queue=queue, best_objective=0, best_node=best_node_ ) assert results.solution_status == "feasible" assert results.termination_condition == "queue_empty" assert results.objective == 0 assert results.bound == (-inf if (sense == minimize) else inf) assert results.absolute_gap == inf assert results.relative_gap == inf assert results.nodes == 0 assert results.wall_time is not None assert results.best_node._uuid == best_node_._uuid if (comm is None) or (comm.size == 1): assert results.best_node is best_node_ if solver.is_worker: assert problem._notify_new_best_node_call_count == 1 assert problem._notify_new_best_node_args[0] is results.best_node assert solver._local_solve_info.explored_nodes_count == 0 assert not problem._notify_new_best_node_args[1] else: assert problem._notify_new_best_node_call_count == 0 best_node_ = Node() best_node_.objective = 1 if (sense == minimize) else -1 best_node_._uuid = "abcd" problem = DummyProblem(sense) results = solver.solve( problem, initialize_queue=queue, best_objective=(2 if (sense == minimize) else -2), best_node=best_node_, ) assert results.solution_status == "feasible" assert results.termination_condition == "queue_empty" assert results.objective == (1 if (sense == minimize) else -1) assert results.bound == (-inf if (sense == minimize) else inf) assert results.absolute_gap == inf assert results.relative_gap == inf assert results.nodes == 0 assert results.wall_time is not None assert results.best_node._uuid == best_node_._uuid if (comm is None) or (comm.size == 1): assert results.best_node is best_node_ if solver.is_worker: assert problem._notify_new_best_node_call_count == 1 assert problem._notify_new_best_node_args[0] is results.best_node assert solver._local_solve_info.explored_nodes_count == 0 assert not problem._notify_new_best_node_args[1] else: assert problem._notify_new_best_node_call_count == 0 # non-empty initial queue root = Node() root._uuid = "abcd" root.tree_depth = 0 root.objective = 0 root.bound = 0 queue = DispatcherQueueData(nodes=[root], worst_terminal_bound=None, sense=minimize) orig_objective = queue.nodes[0].objective for sense in (minimize, maximize): queue.sense = sense queue.nodes[0].objective = orig_objective problem = DummyProblem(sense) results = solver.solve(problem, initialize_queue=queue) assert results.solution_status == "optimal" assert results.termination_condition == "optimality" assert results.objective == 0 assert results.bound == 0 assert results.absolute_gap == 0 assert results.relative_gap == 0 assert results.nodes == 1 assert results.wall_time is not None assert results.best_node._uuid == root._uuid if (comm is None) or (comm.size == 1): assert results.best_node is root if solver.is_worker: assert problem._notify_new_best_node_call_count == 1 assert problem._notify_new_best_node_args[0] is results.best_node if solver._local_solve_info.explored_nodes_count == 1: assert problem._notify_new_best_node_args[1] else: assert solver._local_solve_info.explored_nodes_count == 0 assert not problem._notify_new_best_node_args[1] else: assert problem._notify_new_best_node_call_count == 0 queue.nodes[0].objective = orig_objective problem = DummyProblem(sense) results = solver.solve( problem, initialize_queue=queue, best_objective=(1 if (sense == minimize) else -1), ) assert results.solution_status == "optimal" assert results.termination_condition == "optimality" assert results.objective == 0 assert results.bound == 0 assert results.absolute_gap == 0 assert results.relative_gap == 0 assert results.nodes == 1 assert results.wall_time is not None assert results.best_node._uuid == root._uuid if (comm is None) or (comm.size == 1): assert results.best_node is root if solver.is_worker: assert problem._notify_new_best_node_call_count == 1 assert problem._notify_new_best_node_args[0] is results.best_node if solver._local_solve_info.explored_nodes_count == 1: assert problem._notify_new_best_node_args[1] else: assert solver._local_solve_info.explored_nodes_count == 0 assert not problem._notify_new_best_node_args[1] else: assert problem._notify_new_best_node_call_count == 0 queue.nodes[0].objective = inf if (sense == minimize) else -inf problem = DummyProblem(sense) results = solver.solve( problem, initialize_queue=queue, best_objective=(1 if (sense == minimize) else -1), ) assert results.solution_status == "optimal" assert results.termination_condition == "optimality" assert results.objective == 0 assert results.bound == 0 assert results.absolute_gap == 0 assert results.relative_gap == 0 assert results.nodes == 1 assert results.wall_time is not None assert results.best_node._uuid == root._uuid if (comm is None) or (comm.size == 1): assert results.best_node is root if solver.is_worker: assert problem._notify_new_best_node_call_count == 1 assert problem._notify_new_best_node_args[0] is results.best_node if solver._local_solve_info.explored_nodes_count == 1: assert problem._notify_new_best_node_args[1] else: assert solver._local_solve_info.explored_nodes_count == 0 assert not problem._notify_new_best_node_args[1] else: assert problem._notify_new_best_node_call_count == 0 queue.nodes[0].objective = inf if (sense == minimize) else -inf problem = DummyProblem(sense) results = solver.solve( problem, initialize_queue=queue, best_objective=(1 if (sense == minimize) else -1), disable_objective_call=True, ) assert results.solution_status == "feasible" assert results.termination_condition == "queue_empty" assert results.objective == (1 if (sense == minimize) else -1) assert results.bound == 0 assert results.absolute_gap == 1 assert results.relative_gap == 1 assert results.nodes == 1 assert results.wall_time is not None assert results.best_node is None assert problem._notify_new_best_node_call_count == 0 queue.nodes[0].objective = orig_objective