def _find_root_node_feasible_solution(self, run_id, problem): logger.info(run_id, 'Finding feasible solution at root node') if self.root_node_feasible_solution_seed is not None: seed = self.root_node_feasible_solution_seed logger.info(run_id, 'Use numpy seed {}', seed) np.random.seed(seed) starting_point = [0.0] * problem.num_variables for i, v in enumerate(problem.variables): if problem.has_starting_point(v): starting_point[i] = problem.starting_point(v) elif problem.domain(v).is_integer(): # Variable is integer and will be fixed, but we don't have a # starting point for it. Use lower or upper bound. has_lb = is_inf(problem.lower_bound(v)) has_ub = is_inf(problem.upper_bound(v)) if has_lb: starting_point[i] = problem.lower_bound(v) elif has_ub: starting_point[i] = problem.upper_bound(v) else: starting_point[i] = 0 return solve_primal_with_starting_point( run_id, problem, starting_point, self._nlp_solver )
def compute_branching_point(variable, mip_solution, lambda_): """Compute a convex combination of the midpoint and the solution value. Given a variable $x_i \in [x_i^L, x_i^U]$, with midpoint $x_m = x_i^L + 0.5(x_i^U - x_i^L)$, and its solution value $\bar{x_i}$, branch at $\lambda x_m + (1 - \lambda) \bar{x_i}$. Parameters ---------- variable : VariableView the branching variable mip_solution : Solution the MILP solution lambda_ : float the weight """ x_bar = mip_solution.variables[variable.variable.idx].value lb = variable.lower_bound() ub = variable.upper_bound() # Use special bounds if variable is unbounded user_upper_bound = mc.user_upper_bound if variable.domain.is_integer(): user_upper_bound = mc.user_integer_upper_bound if is_inf(lb): lb = -user_upper_bound if is_inf(ub): ub = user_upper_bound midpoint = lb + 0.5 * (ub - lb) return lambda_ * midpoint + (1.0 - lambda_) * x_bar
def _get_starting_point_and_bounds(self, problem): x0 = np.zeros(problem.num_variables) bounds = [(None, None)] * problem.num_variables for i, var in enumerate(problem.variables): vv = problem.variable_view(var) if vv.is_fixed(): value = vv.value() x0[i] = value bounds[i] = (value, value) else: numerical_lb = lb = vv.lower_bound() if is_inf(lb): lb = None numerical_lb = -mc.user_upper_bound numerical_ub = ub = vv.upper_bound() if is_inf(ub): ub = None numerical_ub = mc.user_upper_bound bounds[i] = (lb, ub) if vv.has_starting_point(): starting_point = vv.starting_point() if is_inf(starting_point): starting_point = max(numerical_lb, min(numerical_ub, 0)) x0[i] = starting_point else: x0[i] = max(numerical_lb, min(numerical_ub, 0)) return x0, bounds
def _unbounded_variable(problem): for var in problem.variables: unbounded = ( is_inf(problem.lower_bound(var)) and is_inf(problem.upper_bound(var)) ) if unbounded: return var return None
def add_initial_solution(self, solution, mc): assert is_inf(self.lower_bound, mc) assert is_inf(self.upper_bound, mc) self._update_node( self.root, solution, is_root_node=True, update_nodes_visited=False, ) self.root.initial_feasible_solution = solution.upper_bound_solution
def get_starting_point_and_bounds(self, run_id, problem): nx = problem.num_variables xi = np.zeros(nx) xl = np.zeros(nx) xu = np.zeros(nx) for i in range(nx): var = problem.variable(i) v = problem.variable_view(i) if v.is_fixed(): xl[i] = v.value() xu[i] = v.value() xi[i] = v.value() else: lb = v.lower_bound() if is_inf(lb): lb = -mc.user_upper_bound logger.warning( run_id, 'Variable {} lower bound is infinite: setting to {}', var.name, lb, ) ub = v.upper_bound() if is_inf(ub): ub = mc.user_upper_bound logger.warning( run_id, 'Variable {} upper bound is infinite: setting to {}', var.name, ub, ) xl[i] = lb xu[i] = ub use_starting_point = False starting_point = 0.0 if v.has_starting_point(): starting_point = v.starting_point() if not is_inf(starting_point): use_starting_point = ( starting_point >= lb and starting_point <= ub ) if use_starting_point: xi[i] = starting_point else: xi[i] = max(lb, min(ub, 0)) return xi, xl, xu
def _before_root_node(self, problem, upper_bound): if self._user_model is None: raise RuntimeError("No user model. Did you call 'before_solve'?") obbt_upper_bound = None if upper_bound is not None and not is_inf(upper_bound): obbt_upper_bound = upper_bound model = self._user_model obbt_start_time = current_time() try: perform_obbt_on_model( model, problem, obbt_upper_bound, timelimit=self.solver.config['obbt_timelimit'], simplex_maxiter=self.solver.config['obbt_simplex_maxiter'], ) self._bac_telemetry.increment_obbt_time( seconds_elapsed_since(obbt_start_time) ) except TimeoutError: logger.info(0, 'OBBT timed out') self._bac_telemetry.increment_obbt_time( seconds_elapsed_since(obbt_start_time) ) return except Exception as ex: logger.warning(0, 'Error performing OBBT: {}', ex) self._bac_telemetry.increment_obbt_time( seconds_elapsed_since(obbt_start_time) ) raise
def perform_fbbt(problem, maxiter, timelimit, objective_upper_bound=None, branching_variable=None): """Perform FBBT on `problem` with the given `maxiter` and `timelimit`.""" bounds = ExpressionDict(problem) bounds_tightener = BoundsTightener( FBBTStopCriterion(max_iter=maxiter, timelimit=timelimit), ) for variable in problem.variables: lb = problem.lower_bound(variable) ub = problem.upper_bound(variable) bounds[variable] = Interval(lb, ub) # For all intents and purposes here an infinite upper bound is the same # as no upper bound. if objective_upper_bound is not None and is_inf(objective_upper_bound): objective_upper_bound = None if objective_upper_bound is not None: root_expr = problem.objective.root_expr expr_bounds = Interval(None, objective_upper_bound) if root_expr not in bounds: bounds[root_expr] = expr_bounds else: existing_bounds = bounds[root_expr] new_bounds = existing_bounds.intersect(expr_bounds) bounds[root_expr] = new_bounds def get_bound(b): def _f(expr): return b[expr] return _f def set_bound(b): def _f(expr, value): b[expr] = value return _f _initialize_bounds(problem, bounds, get_bound(bounds), set_bound(bounds)) try: with timeout(timelimit, 'Timeout in FBBT'): try: bounds_tightener.tighten( problem, bounds, branching_variable=branching_variable, objective_changed=objective_upper_bound is not None, ) except EmptyIntervalError: pass except TimeoutError: pass _manage_infinity_bounds( problem, bounds, get_bound(bounds), set_bound(bounds) ) return bounds
def least_reduced_variable(problem, root_problem): # If any variable is unbounded, branch at 0.0 for v in problem.variables: if is_inf(problem.lower_bound(v)) and is_inf(problem.upper_bound(v)): return problem.variable_view(v) assert problem.num_variables == root_problem.num_variables r = range_ratio(problem, root_problem) # Could not compute range ratio, for example all bounded variables are fixed # Return the first variable to be unbounded. if r is None: for v in problem.variables: if is_inf(problem.lower_bound(v)) or is_inf( problem.upper_bound(v)): return problem.variable_view(v) return None var_idx = np.argmax(r) return problem.variable_view(var_idx)
def _get_constraints(self, problem, problem_eval): constraints = [] for i, constraint in enumerate(problem.constraints): lb = constraint.lower_bound ub = constraint.upper_bound if lb is None or is_inf(lb): constraints.append({ "type": "ineq", "fun": problem_eval.eval_constraint, "jac": problem_eval.eval_constraint_jacobian, "args": [i, ub, True, True], }) elif ub is None or is_inf(ub): constraints.append({ "type": "ineq", "fun": problem_eval.eval_constraint, "jac": problem_eval.eval_constraint_jacobian, "args": [i, lb, True, False], }) elif is_close(lb, ub, atol=mc.epsilon): constraints.append({ "type": "eq", "fun": problem_eval.eval_constraint, "jac": problem_eval.eval_constraint_jacobian, "args": [i, lb, False, False], }) else: # Constraint has both lower and upper bounds, but it's not # an equality constraint. constraints.append({ "type": "ineq", "fun": problem_eval.eval_constraint, "jac": problem_eval.eval_constraint_jacobian, "args": [i, lb, True, False], }) constraints.append({ "type": "ineq", "fun": problem_eval.eval_constraint, "jac": problem_eval.eval_constraint_jacobian, "args": [i, ub, True, True], }) return constraints
def _branch_on_var(self, var): lower_bound = var.lower_bound() domain = var.domain if is_inf(lower_bound): if domain.is_real(): lower_bound = -self.user_upper_bound else: lower_bound = -self.user_integer_upper_bound upper_bound = var.upper_bound() if is_inf(upper_bound): if domain.is_real(): upper_bound = self.user_upper_bound else: upper_bound = self.user_integer_upper_bound step = (upper_bound - lower_bound) / self.k points = step * (np.arange(self.k - 1) + 1.0) + lower_bound assert np.all(np.isfinite(points)) return BranchingPoint(var, points.tolist())
def _manage_infinity_bounds(problem, _bounds, get_bound, set_bound): """In some cases variables bounds are numbers that are bigger than mc.infinity. Change them back to None. """ for variable in problem.variables: expr_bounds = get_bound(variable) lower_bound = expr_bounds.lower_bound upper_bound = expr_bounds.upper_bound if is_inf(lower_bound): new_lower_bound = None else: new_lower_bound = lower_bound if is_inf(upper_bound): new_upper_bound = None else: new_upper_bound = upper_bound set_bound(variable, Interval(new_lower_bound, new_upper_bound))
def range_ratio(problem, root_problem): assert problem.num_variables == root_problem.num_variables lower_bounds = np.array(problem.lower_bounds) upper_bounds = np.array(problem.upper_bounds) root_lower_bounds = np.array(root_problem.lower_bounds) root_upper_bounds = np.array(root_problem.upper_bounds) denominator = np.abs(root_upper_bounds - root_lower_bounds) + mc.epsilon numerator = upper_bounds - lower_bounds numerator_mask = ~is_inf(numerator) denominator_mask = ~is_inf(denominator) finite_numerator = np.zeros_like(numerator) finite_denominator = np.zeros_like(denominator) + mc.epsilon finite_numerator[numerator_mask] = numerator[numerator_mask] finite_denominator[denominator_mask] = denominator[denominator_mask] # All bounded variables are fixed, range ratio is not possible to compute if is_close(np.sum(np.abs(finite_numerator)), 0.0, atol=mc.epsilon): return None return np.nan_to_num(finite_numerator / finite_denominator)
def _compute_variable_starting_point_as_float(var, mc, value): # Use starting point if present if value is not None: return value # If var has both bounds, use midpoint lb = var.lb if is_inf(lb, mc): lb = None ub = var.ub if is_inf(ub, mc): ub = None if lb is not None and ub is not None: return lb + 0.5 * (ub - lb) # If unbounded, use 0 if lb is None and lb is None: return 0.0 # If no lower bound, use upper bound if lb is None: return ub # Otherwise, use lower bound return lb
def has_converged(self, state): if self._relaxation_is_linear: return True if self._nlp_solution is None: return False if is_inf(state.lower_bound): return False return is_close( state.lower_bound, self._nlp_solution, rtol=self.convergence_relative_tol, )
def _compute_gamma(self, optimal_obj, value): # Unknown primal if value is None or is_inf(value): return 1.0 # |opt| = |obj| = 0.0 if is_close(np.abs(optimal_obj), 0.0, atol=mc.epsilon): if is_close(np.abs(value), 0.0, atol=mc.epsilon): return 0.0 # opt * obj < 0 if np.sign(optimal_obj) * np.sign(value) < 0: return 1.0 num = np.abs(optimal_obj - value) den = max(np.abs(optimal_obj), np.abs(value)) return num / den