Beispiel #1
0
    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
Beispiel #2
0
 def _add_work_to_queue(self, node_data, set_tree_id=True):
     if set_tree_id:
         assert not Node._has_tree_id(node_data)
         Node._insert_tree_id(node_data, self.next_tree_id)
         self.next_tree_id += 1
     else:
         assert Node._has_tree_id(node_data)
         assert Node._extract_tree_id(node_data) < self.next_tree_id
     bound = Node._extract_bound(node_data)
     if self.converger.eligible_for_queue(bound, self.best_objective):
         self.queue.put(node_data)
         return True
     else:
         self._check_update_worst_terminal_bound(bound)
         return False
Beispiel #3
0
 def test_tree_id(self):
     node = Node()
     assert node.tree_id is None
     assert node.parent_tree_id is None
     Node._insert_tree_id(node._data, 1)
     assert node.tree_id == 1
     assert node.parent_tree_id is None
     Node._insert_parent_tree_id(node._data, 2)
     assert node.tree_id == 1
     assert node.parent_tree_id == 2
     Node._clear_tree_id(node._data)
     assert node.tree_id is None
     assert node.parent_tree_id == 2
     Node._clear_parent_tree_id(node._data)
     assert node.tree_id is None
     assert node.parent_tree_id is None
Beispiel #4
0
    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
Beispiel #5
0
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)
Beispiel #6
0
 def test_children(self):
     parent = Node()
     assert parent.queue_priority is None
     assert parent.tree_id is None
     assert parent.parent_tree_id is None
     assert parent.tree_depth == 0
     assert len(parent.state) == 0
     parent.queue_priority = 10
     assert parent.queue_priority == 10
     assert parent.tree_id is None
     assert parent.parent_tree_id is None
     assert parent.tree_depth == 0
     assert len(parent.state) == 0
     parent.bound = -1
     assert parent.queue_priority == 10
     assert parent.tree_id is None
     assert parent.parent_tree_id is None
     assert parent.tree_depth == 0
     assert parent.bound == -1
     parent.objective = -2
     assert parent.queue_priority == 10
     assert parent.tree_id is None
     assert parent.parent_tree_id is None
     assert parent.tree_depth == 0
     assert parent.bound == -1
     assert parent.objective == -2
     assert len(parent.state) == 0
     parent.resize(5)
     assert parent.queue_priority == 10
     assert parent.tree_id is None
     assert parent.parent_tree_id is None
     assert parent.tree_depth == 0
     assert parent.bound == -1
     assert parent.objective == -2
     assert len(parent.state) == 5
     children = [parent.new_child() for i in range(3)]
     assert len(children) == 3
     for child in children:
         assert child.queue_priority is None
         assert child.tree_id is None
         assert child.parent_tree_id is None
         assert child.tree_depth == 1
         assert child.bound == -1
         assert child.objective == -2
         assert len(child.state) == 5
     Node._insert_tree_id(parent._data, 0)
     assert parent.tree_id == 0
     children = [parent.new_child() for i in range(3)]
     assert len(children) == 3
     for child in children:
         assert child.queue_priority is None
         assert child.tree_id is None
         assert child.parent_tree_id == 0
         assert child.tree_depth == 1
         assert child.bound == -1
         assert child.objective == -2
         assert len(child.state) == 5
     children = [parent.new_child(size=10) for i in range(4)]
     assert len(children) == 4
     for child in children:
         assert child.queue_priority is None
         assert child.tree_id is None
         assert child.parent_tree_id == 0
         assert child.tree_depth == 1
         assert child.bound == -1
         assert child.objective == -2
         assert len(child.state) == 10
Beispiel #7
0
    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