def test_initialize_queue(self): node_limit = None time_limit = None log = get_simple_logger() log_interval_seconds = inf log_new_incumbent = True convergence_checker = ConvergenceChecker(minimize) root = Node(size=0) Node._insert_tree_id(root._data, 0) root.bound = convergence_checker.unbounded_objective root.objective = convergence_checker.infeasible_objective queue = DispatcherQueueData(nodes=[root], next_tree_id=1) disp = DispatcherLocal() disp.initialize(0, queue, 'bound', convergence_checker, node_limit, time_limit, log, log_interval_seconds, log_new_incumbent) assert disp.best_objective == 0 disp.initialize(1, queue, 'bound', convergence_checker, node_limit, time_limit, log, log_interval_seconds, log_new_incumbent) assert disp.best_objective == 1 root.objective = -1 disp.initialize(1, queue, 'bound', convergence_checker, node_limit, time_limit, log, log_interval_seconds, log_new_incumbent) assert disp.best_objective == -1
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)
def _logging_redirect_check(comm): opt = Solver(comm=comm) p = DummyProblem() log = logging.Logger(None, level=logging.WARNING) log.addHandler(_RedirectHandler(opt._disp)) if opt.is_dispatcher: assert (comm is None) or (comm.rank == 0) root = Node() p.save_state(root) root.objective = p.infeasible_objective() root.bound = p.unbounded_objective() initialize_queue = DispatcherQueueData(nodes=[root], worst_terminal_bound=None, sense=p.sense()) out = StringIO() formatter = logging.Formatter("[%(levelname)s] %(message)s") opt._disp.initialize( p.infeasible_objective(), None, initialize_queue, "bound", ConvergenceChecker(p.sense()), None, None, None, True, get_simple_logger(console=True, stream=out, level=logging.DEBUG, formatter=formatter), 0.0, True, ) log.debug("0: debug") log.info("0: info") log.warning("0: warning") log.error("0: error") log.critical("0: critical") if (comm is not None) and (comm.size > 1): opt._disp.serve() else: assert comm is not None if comm.size > 1: for i in range(1, comm.size): if comm.rank == i: log.debug(str(comm.rank) + ": debug") log.info(str(comm.rank) + ": info") log.warning(str(comm.rank) + ": warning") log.error(str(comm.rank) + ": error") log.critical(str(comm.rank) + ": critical") opt.worker_comm.Barrier() if opt.worker_comm.rank == 0: opt._disp.stop_listen() if comm is not None: comm.Barrier() if opt.is_dispatcher: assert ("\n".join( out.getvalue().splitlines()[7:])) == _get_logging_baseline( comm.size if comm is not None else 1)
def test_queue_strategy(self): node_limit = None time_limit = None log = get_simple_logger() log_interval_seconds = inf log_new_incumbent = True convergence_checker = ConvergenceChecker(minimize) root = Node(size=0) Node._insert_tree_id(root._data, 0) root.bound = convergence_checker.unbounded_objective root.objective = convergence_checker.infeasible_objective queue = DispatcherQueueData(nodes=[root], next_tree_id=1) disp = DispatcherLocal() disp.initialize(inf, queue, 'bound', convergence_checker, node_limit, time_limit, log, log_interval_seconds, log_new_incumbent) assert type(disp.queue) is WorstBoundFirstPriorityQueue disp.initialize(inf, queue, 'custom', convergence_checker, node_limit, time_limit, log, log_interval_seconds, log_new_incumbent) assert type(disp.queue) is CustomPriorityQueue disp.initialize(inf, queue, 'objective', convergence_checker, node_limit, time_limit, log, log_interval_seconds, log_new_incumbent) assert type(disp.queue) is BestObjectiveFirstPriorityQueue disp.initialize(inf, queue, 'breadth', convergence_checker, node_limit, time_limit, log, log_interval_seconds, log_new_incumbent) assert type(disp.queue) is BreadthFirstPriorityQueue disp.initialize(inf, queue, 'depth', convergence_checker, node_limit, time_limit, log, log_interval_seconds, log_new_incumbent) assert type(disp.queue) is DepthFirstPriorityQueue disp.initialize(inf, queue, 'fifo', convergence_checker, node_limit, time_limit, log, log_interval_seconds, log_new_incumbent) assert type(disp.queue) is FIFOQueue disp.initialize(inf, queue, 'random', convergence_checker, node_limit, time_limit, log, log_interval_seconds, log_new_incumbent) assert type(disp.queue) is RandomPriorityQueue disp.initialize(inf, queue, 'local_gap', convergence_checker, node_limit, time_limit, log, log_interval_seconds, log_new_incumbent) assert type(disp.queue) is LocalGapPriorityQueue
def _logging_check(comm): opt = Solver(comm=comm) p = DummyProblem() if opt.is_dispatcher: assert (comm is None) or (comm.rank == 0) root = Node() p.save_state(root) root.objective = p.infeasible_objective() root.bound = p.unbounded_objective() assert root.tree_id is None Node._insert_tree_id(root._data, 0) initialize_queue = DispatcherQueueData(nodes=[root], next_tree_id=1) out = StringIO() formatter = logging.Formatter("[%(levelname)s] %(message)s") opt._disp.initialize( p.infeasible_objective(), initialize_queue, "bound", ConvergenceChecker(p.sense()), None, None, get_simple_logger(console=True, stream=out, level=logging.DEBUG, formatter=formatter), 0.0, True) opt._disp.log_debug("0: debug") opt._disp.log_info("0: info") opt._disp.log_warning("0: warning") opt._disp.log_error("0: error") if (comm is not None) and (comm.size > 1): opt._disp.serve() else: assert comm is not None if comm.size > 1: for i in range(1, comm.size): if comm.rank == i: opt._disp.log_debug(str(comm.rank) + ": debug") opt._disp.log_info(str(comm.rank) + ": info") opt._disp.log_warning(str(comm.rank) + ": warning") opt._disp.log_error(str(comm.rank) + ": error") opt.worker_comm.Barrier() if opt.worker_comm.rank == 0: opt._disp.stop_listen() if comm is not None: comm.Barrier() if opt.is_dispatcher: assert ('\n'.join(out.getvalue().splitlines()[8:])) == \ _get_logging_baseline(comm.size if comm is not None else 1)
def test_queue_strategy(self): node_limit = None time_limit = None queue_limit = None track_bound = True log = get_simple_logger() log_interval_seconds = inf log_new_incumbent = True convergence_checker = ConvergenceChecker(minimize) root = Node() root.tree_depth = 0 root.bound = convergence_checker.unbounded_objective root.objective = convergence_checker.infeasible_objective queue = DispatcherQueueData([root], None, minimize) disp = DispatcherLocal() disp.initialize( convergence_checker.infeasible_objective, None, queue, "bound", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert type(disp.queue) is WorstBoundFirstPriorityQueue disp.initialize( convergence_checker.infeasible_objective, None, queue, "custom", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert type(disp.queue) is CustomPriorityQueue disp.initialize( convergence_checker.infeasible_objective, None, queue, "objective", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert type(disp.queue) is BestObjectiveFirstPriorityQueue disp.initialize( convergence_checker.infeasible_objective, None, queue, "breadth", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert type(disp.queue) is BreadthFirstPriorityQueue disp.initialize( convergence_checker.infeasible_objective, None, queue, "depth", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert type(disp.queue) is DepthFirstPriorityQueue disp.initialize( convergence_checker.infeasible_objective, None, queue, "fifo", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert type(disp.queue) is FIFOQueue disp.initialize( convergence_checker.infeasible_objective, None, queue, "lifo", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert type(disp.queue) is LIFOQueue disp.initialize( convergence_checker.infeasible_objective, None, queue, "random", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert type(disp.queue) is RandomPriorityQueue disp.initialize( convergence_checker.infeasible_objective, None, queue, "local_gap", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert type(disp.queue) is LocalGapPriorityQueue disp.initialize( convergence_checker.infeasible_objective, None, queue, ("bound", "local_gap"), convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert type(disp.queue) is LexicographicPriorityQueue
def test_bound(self): for sense in (minimize, maximize): queue = DispatcherQueueData([], None, sense) assert queue.nodes == [] assert queue.worst_terminal_bound is None assert queue.sense == sense assert queue.bound() is None queue = DispatcherQueueData([], 0, sense) assert queue.nodes == [] assert queue.worst_terminal_bound == 0 assert queue.sense == sense assert queue.bound() == 0 queue = DispatcherQueueData([Node()], 0, sense) queue.nodes[0].bound = -1 if (sense == minimize) else 1 assert len(queue.nodes) == 1 assert queue.worst_terminal_bound == 0 assert queue.sense == sense assert queue.bound() == (-1 if (sense == minimize) else 1) queue.worst_terminal_bound = -2 if (sense == minimize) else 2 assert queue.bound() == (-2 if (sense == minimize) else 2)
def test_initialize_queue(self): node_limit = None time_limit = None queue_limit = None track_bound = True log = get_simple_logger() log_interval_seconds = inf log_new_incumbent = True convergence_checker = ConvergenceChecker(minimize) root = Node() root.tree_depth = 0 root.bound = convergence_checker.unbounded_objective root.objective = convergence_checker.infeasible_objective queue = DispatcherQueueData([root], None, minimize) best_node_ = Node() best_node_.objective = 0 best_node_._generate_uuid() disp = DispatcherLocal() disp.initialize( convergence_checker.infeasible_objective, best_node_, queue, "bound", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert disp.best_objective == 0 assert disp.best_node.objective == 0 assert disp.best_node is best_node_ disp.initialize( -1, best_node_, queue, "bound", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert disp.best_objective == -1 assert disp.best_node.objective == 0 assert disp.best_node is best_node_ best_node_.objective = 1 disp.initialize( 2, best_node_, queue, "bound", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert disp.best_objective == 1 assert disp.best_node.objective == 1 assert disp.best_node is best_node_ best_node_.objective = 1 root.objective = -1 disp.initialize( 2, best_node_, queue, "bound", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert disp.best_objective == 1 assert disp.best_node.objective == 1 assert disp.best_node is best_node_ best_node_.objective = 1 root.objective = -1 disp.initialize( -2, best_node_, queue, "bound", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, ) assert disp.best_objective == -2 assert disp.best_node.objective == 1 assert disp.best_node is best_node_ # bad objective sense queue = DispatcherQueueData([root], None, maximize) with pytest.raises(ValueError): disp.initialize( convergence_checker.infeasible_objective, best_node_, queue, "bound", convergence_checker, node_limit, time_limit, queue_limit, track_bound, log, log_interval_seconds, log_new_incumbent, )
def solve(self, problem, best_objective=None, disable_objective_call=False, absolute_gap=1e-8, relative_gap=1e-4, scale_function=_default_scale, queue_tolerance=0, branch_tolerance=0, comparison_tolerance=0, objective_stop=None, bound_stop=None, node_limit=None, time_limit=None, initialize_queue=None, queue_strategy="bound", log_interval_seconds=1.0, log_new_incumbent=True, log=_notset): """Solve a problem using branch-and-bound. Note ---- Parameters for this function are treated differently depending on whether the process is a worker or dispatcher. For the serial case (no MPI), the single process is both a worker and a dispatcher. For the parallel case, exactly one process is a dispatcher and all processes are workers. A **(W)** in the parameter description indicates that it is only used by worker processes (ignored otherwise). A **(D)** in the parameter description indicates that it is only used by the dispatcher process (ignored otherwise). An **(A)** indicates that it is used by all processes, and it is assumed the same value is provided for each process; otherwise, the behavior is undefined. Parameters ---------- problem : :class:`pybnb.Problem <pybnb.problem.Problem>` An object defining a branch-and-bound problem. best_objective : float, optional Initializes the solve with an assumed best objective. This is the only option used by both worker and dispatcher processes that can be set to a different value on each process. The dispatcher will collect all values and use the best. (default: None) disable_objective_call : bool, optional **(W)** Disables requests for an objective value from subproblems. (default: False) absolute_gap : float, optional **(A)** The maximum absolute difference between the global bound and best objective for the problem to be considered solved to optimality. Setting to `None` will disable this optimality check. (default: 1e-8) relative_gap : float, optional **(A)** The maximum relative difference (absolute difference scaled by `max{1.0,|objective|}`) between the global bound and best objective for the problem to be considered solved to optimality. Setting to `None` will disable this optimality check. (default: 1e-4) scale_function : function, optional **(A)** A function with signature `f(bound, objective) -> float` that returns a positive scale factor used to convert the absolute difference between the bound and objective into a relative difference. The relative difference is compared with the `relative_gap` convergence tolerance to determine if the solver should terminate. The default is equivalent to `max{1.0,|objective|}`. Other examples one could use are `max{|bound|,|objective|}`, `(|bound|+|objective|)/2`, etc. queue_tolerance : float, optional **(A)** The absolute tolerance used when deciding if a node is eligible to enter the queue. The difference between the node bound and the incumbent objective must be greater than this value. The default setting of zero means that nodes whose bound is equal to the incumbent objective are not eligible to enter the queue. Setting this to larger values can be used to control the queue size, but it should be kept small enough to allow absolute and relative optimality tolerances to be met. This option can also be set to `None` to allow nodes with a bound equal to (but not greater than) the incumbent objective to enter the queue. (default: 0) branch_tolerance : float, optional **(A)** The absolute tolerance used when deciding if the computed objective and bound for a node are sufficiently different to branch into the node. The default value of zero means that branching will occur if the bound is not exactly equal to the objective. This option can be set to `None` to enable branching for nodes with a bound and objective that are exactly equal. (default: 0) comparison_tolerance : float, optional **(A)** The absolute tolerance used when deciding if two objective or bound values are sufficiently different to be considered improved or worsened. This tolerance controls when the solver considers a new incumbent objective to be found. It also controls when warnings are output about bounds becoming worse on child nodes. Setting this to larger values can be used to avoid the above solver actions due to insignificant numerical differences, but it is better to deal with these numerical issues by rounding numbers to a reliable precision before returning them from the problem methods. (default: 0) objective_stop : float, optional **(A)** If provided, the solve will terminate when a feasible objective is found that is at least as good as the specified value, and the termination_condition flag on the results object will be set to "objective_limit". If this value is infinite, the solve will terminate as soon as a finite objective is found. (default: None) bound_stop : float, optional **(A)** If provided, the solve will terminate when the global bound on the objective is at least as good as the specified value, and the termination_condition flag on the results object will be set to "objective_limit". If this value is infinite, the solve will terminate as soon as a finite bound is found. (default: None) node_limit : int, optional **(D)** If provided, the solve will begin to terminate once this many nodes have been served from the dispatcher queue, and the termination_condition flag on the results object will be set to "node_limit". (default: None) time_limit : float, optional **(D)** If provided, the solve will begin to terminate once this amount of time has passed, and the termination_condition flag on the results object will be set to "time_limit". Note that the solve may run for an arbitrarily longer amount of time, depending how long worker processes spend completing their final task. (default: None) initialize_queue : :class:`pybnb.dispatcher.DispatcherQueueData`, optional **(D)** Initializes the dispatcher queue with that remaining from a previous solve (obtained by calling :func:`Solver.save_dispatcher_queue` after the solve). If left as None, the queue will be initialized with a single root node created by calling :func:`problem.save_state <pybnb.problem.Problem.save_state>`. (default: None) queue_strategy : :class:`QueueStrategy <pybnb.common.QueueStrategy>` **(D)** Sets the strategy for prioritizing nodes in the central dispatcher queue. See the :class:`QueueStrategy <pybnb.common.QueueStrategy>` enum for the list of acceptable values. This keyword can be assigned one of the enumeration attributes or an equivalent string name. (default: "bound") log_interval_seconds : float, optional **(D)** The approximate time (in seconds) between solver log updates. More time may pass between log updates if no updates have been received from worker processes, and less time may pass if a new incumbent objective is found. (default: 1.0) log_new_incumbent : bool **(D)** Controls whether updates to the best objective are logged immediately (overriding the log interval). Setting this to false can be useful when frequent updates to the incumbent are expected and the additional logging slows down the dispatcher. (default: True) log : logging.Logger, optional **(D)** A log object where solver output should be sent. The default value causes all output to be streamed to the console. Setting to None disables all output. Returns ------- results : :class:`SolverResults` An object storing information about the solve. """ self._reset_local_solve_stats() self._solve_start = self._time() assert (initialize_queue is None) or \ (self.is_dispatcher) if best_objective is None: best_objective = problem.infeasible_objective() results = SolverResults() convergence_checker = ConvergenceChecker( problem.sense(), absolute_gap=absolute_gap, relative_gap=relative_gap, scale_function=scale_function, queue_tolerance=queue_tolerance, branch_tolerance=branch_tolerance, comparison_tolerance=comparison_tolerance, objective_stop=objective_stop, bound_stop=bound_stop) problem.notify_solve_begins(self.comm, self.worker_comm, convergence_checker) root = Node() root.queue_priority = 0 problem.save_state(root) try: if self.is_dispatcher: if initialize_queue is None: root.bound = problem.unbounded_objective() root.objective = best_objective assert root.tree_id is None Node._insert_tree_id(root._data, 0) initialize_queue = DispatcherQueueData( nodes=[Node(data_=root._data.copy())], next_tree_id=1) if log is _notset: log = get_simple_logger() if type(queue_strategy) is \ QueueStrategy: queue_strategy = \ queue_strategy.value self._disp.initialize( best_objective, initialize_queue, queue_strategy, convergence_checker, node_limit, time_limit, log, log_interval_seconds, log_new_incumbent) if not self.is_worker: def handler(signum, frame): #pragma:nocover self._disp.log_warning( "Solve interrupted by user. " "Waiting for current worker " "jobs to complete before " "terminating the solve.") self._disp.termination_condition = \ TerminationCondition.interrupted with MPI_InterruptHandler(handler): tmp = self._disp.serve() else: def handler(signum, frame): #pragma:nocover if self.is_dispatcher: self._disp.log_warning( "Solve interrupted by user. " "Waiting for current worker " "jobs to complete before " "terminating the solve.") self._disp.termination_condition = \ TerminationCondition.interrupted with MPI_InterruptHandler(handler): tmp = self._solve(problem, best_objective, disable_objective_call, convergence_checker, results) if not self.is_dispatcher: self._disp.clear_cache() (results.objective, results.bound, results.termination_condition, self._global_solve_info) = tmp results.nodes = self._global_solve_info.explored_nodes_count self._fill_results(results, convergence_checker) except: #pragma:nocover sys.stderr.write("Exception caught: "+str(sys.exc_info()[1])+"\n") sys.stderr.write("Attempting to shut down, but this may hang.\n") sys.stderr.flush() raise finally: problem.load_state(root) self._wall_time = self._time() - self._solve_start results.wall_time = self._wall_time assert results.solution_status in SolutionStatus,\ str(results) assert results.termination_condition in TerminationCondition,\ str(results) problem.notify_solve_finished(self.comm, self.worker_comm, results) if self.is_dispatcher and \ (log is not None) and \ (not log.disabled): self._disp.log_info("") if results.solution_status in ("feasible", "optimal"): agap = convergence_checker.compute_absolute_gap( results.bound, results.objective) rgap = convergence_checker.compute_relative_gap( results.bound, results.objective) if results.solution_status == "feasible": self._disp.log_info("Feasible solution found") else: if (convergence_checker.absolute_gap is not None) and \ agap <= convergence_checker.absolute_gap: self._disp.log_info("Absolute optimality tolerance met") if (convergence_checker.relative_gap is not None) and \ rgap <= convergence_checker.relative_gap: self._disp.log_info("Relative optimality tolerance met") assert results.solution_status == "optimal" self._disp.log_info("Optimal solution found!") elif results.solution_status == "infeasible": self._disp.log_info("Problem is infeasible") elif results.solution_status == "unbounded": self._disp.log_info("Problem is unbounded") elif results.solution_status == "invalid": #pragma:nocover self._disp.log_info("Problem is invalid") else: assert results.solution_status == "unknown" self._disp.log_info("Status unknown") self._disp.log_info("") self._disp.log_info(str(results)) return 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