def _find_split_inequality(self: G, idx: int, **kwargs: Any): assert idx in self._integer_indices, 'must lift and project on integer index' x_idx = self.solution[idx] assert self._is_fractional( x_idx), "must lift and project on index with fractional value" # build the CGLP model from ISE 418 Lecture 15 Slide 7 but for LP with >= constraints lp = CyClpSimplex() # declare variables pi = lp.addVariable('pi', self.lp.nVariables) pi0 = lp.addVariable('pi0', 1) u1 = lp.addVariable('u1', self.lp.nConstraints) u2 = lp.addVariable('u2', self.lp.nConstraints) w1 = lp.addVariable('w1', self.lp.nVariables) w2 = lp.addVariable('w2', self.lp.nVariables) # set bounds lp += u1 >= CyLPArray(np.zeros(self.lp.nConstraints)) lp += u2 >= CyLPArray(np.zeros(self.lp.nConstraints)) w_ub = CyLPArray(np.zeros(self.lp.nVariables)) w_ub[idx] = float('inf') lp += w_ub >= w1 >= CyLPArray(np.zeros(self.lp.nVariables)) lp += w_ub >= w2 >= CyLPArray(np.zeros(self.lp.nVariables)) # set constraints # (pi, pi0) must be valid for both parts of the disjunction lp += 0 >= -pi + self.lp.coefMatrix.T * u1 - w1 lp += 0 >= -pi + self.lp.coefMatrix.T * u2 + w2 lp += 0 <= -pi0 + CyLPArray( self.lp.constraintsLower) * u1 - floor(x_idx) * w1.sum() lp += 0 <= -pi0 + CyLPArray( self.lp.constraintsLower) * u2 + ceil(x_idx) * w2.sum() # normalize variables lp += u1.sum() + u2.sum() + w1.sum() + w2.sum() == 1 # set objective: find the deepest cut # since pi * x >= pi0 for all x in disjunction, we want min pi * x_star - pi0 lp.objective = CyLPArray(self.solution) * pi - pi0 # solve lp.primal(startFinishOptions='x') assert lp.getStatusCode() == 0, 'we should get optimal solution' assert lp.objectiveValue <= 0, 'pi * x >= pi -> pi * x - pi >= 0 -> ' \ 'negative objective at x^* since it gets cut off' # get solution return lp.primalVariableSolution['pi'], lp.primalVariableSolution[ 'pi0']
def solve_cylp(model, B_vectors, weights, ray, chunksize): """ Worker process for LP_solver_cylp_mp. Parameters ---------- model : CyClpModel Model of the LP Problem, see :py:func:`LP_solver_cylp_mp` B_vectors : matrix Matrix containing B vectors, see :py:func:`construct_B_vectors` weights : array Weights. ray : int Starting ray. chunksize : int Number of rays to process. Returns ------- soln : array Solution to LP problem. See Also -------- LP_solver_cylp_mp : Parent function. LP_solver_cylp : Single Process Solver. """ from cylp.cy.CyClpSimplex import CyClpSimplex from cylp.py.modeling.CyLPModel import CyLPModel, CyLPArray n_gates = weights.shape[1] // 2 n_rays = B_vectors.shape[0] soln = np.zeros([chunksize, n_gates]) # import LP model in solver s = CyClpSimplex(model) # disable logging in multiprocessing anyway s.logLevel = 0 i = 0 for raynum in range(ray, ray + chunksize): # set new B_vector values for actual ray s.setRowLowerArray(np.squeeze(np.asarray(B_vectors[raynum]))) # set new weights (objectives) for actual ray s.setObjectiveArray(np.squeeze(np.asarray(weights[raynum]))) # solve with dual method, it is faster s.dual() # extract primal solution soln[i, :] = s.primalVariableSolution['x'][n_gates:2 * n_gates] i = i + 1 return soln
def solve_cylp(model, B_vectors, weights, ray, chunksize): """ Worker process for LP_solver_cylp_mp. Parameters ---------- model : CyClpModel Model of the LP Problem, see :py:func:`LP_solver_cylp_mp` B_vectors : matrix Matrix containing B vectors, see :py:func:`construct_B_vectors` weights : array Weights. ray : int Starting ray. chunksize : int Number of rays to process. Returns ------- soln : array Solution to LP problem. See Also -------- LP_solver_cylp_mp : Parent function. LP_solver_cylp : Single Process Solver. """ from cylp.cy.CyClpSimplex import CyClpSimplex from cylp.py.modeling.CyLPModel import CyLPModel, CyLPArray n_gates = weights.shape[1] // 2 n_rays = B_vectors.shape[0] soln = np.zeros([chunksize, n_gates]) # import LP model in solver s = CyClpSimplex(model) # disable logging in multiprocessing anyway s.logLevel = 0 i = 0 for raynum in range(ray, ray + chunksize): # set new B_vector values for actual ray s.setRowLowerArray(np.squeeze(np.asarray(B_vectors[raynum]))) # set new weights (objectives) for actual ray s.setObjectiveArray(np.squeeze(np.asarray(weights[raynum]))) # solve with dual method, it is faster s.dual() # extract primal solution soln[i, :] = s.primalVariableSolution['x'][n_gates: 2 * n_gates] i = i + 1 return soln
def __init__(self: T, lp: CyClpSimplex, integer_indices: List[int], idx: int = None, lower_bound: Union[float, int] = -float('inf'), b_idx: int = None, b_dir: str = None, b_val: float = None, depth: int = 0, ancestors: tuple = None): """ :param lp: model object simplex is run against. Assumed Ax >= b :param idx: index of this node (e.g. in the branch and bound tree) :param integer_indices: indices of variables we aim to find integer solutions :param lower_bound: starting lower bound on optimal objective value for the minimization problem in this node :param b_idx: index of the branching variable :param b_dir: direction of branching :param b_val: initial value of the branching variable :param depth: how deep in the tree this node is :param ancestors: tuple of nodes that preceded this node (e.g. were branched on to create this node) """ assert isinstance(lp, CyClpSimplex), 'lp must be CyClpSimplex instance' assert all(0 <= idx < lp.nVariables and isinstance(idx, int) for idx in integer_indices), 'indices must match variables' assert idx is None or isinstance( idx, int), 'node idx must be integer if provided' assert len(set(integer_indices)) == len(integer_indices), \ 'indices must be distinct' assert isinstance(lower_bound, float) or isinstance(lower_bound, int), \ 'lower bound must be a float or an int' assert (b_dir is None) == (b_idx is None) == (b_val is None), \ 'none are none or all are none' assert b_idx in integer_indices or b_idx is None, \ 'branch index corresponds to integer variable if it exists' assert b_dir in ['right', 'left'] or b_dir is None, \ 'we can only branch right or left' if b_val is not None: good_left = 0 < b_val - lp.variablesUpper[b_idx] < 1 good_right = 0 < lp.variablesLower[b_idx] - b_val < 1 assert (b_dir == 'left' and good_left) or \ (b_dir == 'right' and good_right), 'branch val should be within 1 of both bounds' assert isinstance(depth, int) and depth >= 0, 'depth is a positive integer' if ancestors is not None: assert isinstance(ancestors, tuple), 'ancestors must be a tuple if provided' assert idx not in ancestors, 'idx cannot be an ancestor of itself' lp.logLevel = 0 self.lp = lp self._integer_indices = integer_indices self.idx = idx self._var_indices = list(range(lp.nVariables)) self._row_indices = list(range(lp.nConstraints)) self.lower_bound = lower_bound self.objective_value = None self.solution = None self.lp_feasible = None self.unbounded = None self.mip_feasible = None self._epsilon = .0001 self._b_dir = b_dir self._b_idx = b_idx self._b_val = b_val self.depth = depth self.search_method = 'best first' self.branch_method = 'most fractional' self.is_leaf = True ancestors = ancestors or tuple() idx_tuple = (idx, ) if idx is not None else tuple() self.lineage = ancestors + idx_tuple or None # test all 4 ways this can pan out
def _base_branch(self: T, branch_idx: int, next_node_idx: int = None, **kwargs: Any) -> Dict[str, T]: """ Creates two new copies of the node with new bounds placed on the variable with index <idx>, one with the variable's lower bound set to the ceiling of its current value and another with the variable's upper bound set to the floor of its current value. :param branch_idx: index of variable to branch on :param next_node_idx: index that should be assigned to the next node created. If left None, assign no indices to both child nodes. :return: dict of Nodes with the new bounds keyed by direction they branched """ assert next_node_idx is None or isinstance(next_node_idx, int), \ 'next node index should be integer if provided' assert self.lp_feasible, 'must solve before branching' assert branch_idx in self._integer_indices, 'must branch on integer index' b_val = self.solution[branch_idx] assert self._is_fractional( b_val), "index branched on must be fractional" self.is_leaf = False # get end basis to warm start the children # appears to be tuple (variable statuses, slack statuses) basis = self.lp.getBasisStatus() # create new lp's for each direction children = {'right': CyClpSimplex(), 'left': CyClpSimplex()} for direction, lp in children.items(): x = lp.addVariable('x', self.lp.nCols) l = CyLPArray(self.lp.variablesLower.copy()) u = CyLPArray(self.lp.variablesUpper.copy()) if direction == 'left': u[branch_idx] = floor(b_val) else: l[branch_idx] = ceil(b_val) lp += l <= x <= u lp += CyLPArray(self.lp.constraintsLower.copy()) <= self.lp.coefMatrix * x \ <= CyLPArray(self.lp.constraintsUpper.copy()) lp.objective = self.lp.objective lp.setBasisStatus(*basis) # warm start # return instances of the subclass that calls this function return { 'left': type(self)(lp=children['left'], integer_indices=self._integer_indices, idx=next_node_idx, lower_bound=self.objective_value, b_idx=branch_idx, b_dir='left', b_val=b_val, depth=self.depth + 1, ancestors=self.lineage), 'right': type(self)(lp=children['right'], integer_indices=self._integer_indices, idx=next_node_idx + 1 if next_node_idx is not None else next_node_idx, lower_bound=self.objective_value, b_idx=branch_idx, b_dir='right', b_val=b_val, depth=self.depth + 1, ancestors=self.lineage), 'next_node_idx': next_node_idx + 2 if next_node_idx is not None else next_node_idx }
def find_strong_disjunctive_cut(self, root_id: int) -> Tuple[CyLPArray, float]: """ Generate a strong cut valid for the disjunction encoded in the subtree rooted at node <root_id>. This cut is optimized to maximize the violation of the LP relaxation solution at node <root_id> see ISE 418 Lecture 13 slide 3, Lecture 14 slide 9, and Lecture 15 slides 6-7 for derivation :param root_id: id of the node off which we will base the disjunction :return: a valid inequality (pi, pi0), i.e. pi^T x >= pi0 for all x in the convex hull of the disjunctive terms' LP relaxations """ # sanity checks assert root_id in self.tree, 'parent must already exist in tree' root = self.tree.get_node_instances(root_id) # get each disjunctive term terminal_nodes = self.tree.get_leaves(root_id) # terminal nodes pruned for infeasibility do not expand disjunction, so remove them disjunctive_nodes = {n.idx: n for n in terminal_nodes if n.lp_feasible is not False} var_dicts = [{v.name: v.dim for v in n.lp.variables} for n in disjunctive_nodes.values()] assert all(var_dicts[0] == d for d in var_dicts), \ 'Each disjunctive term should have the same variables. The feature allowing' \ ' otherwise remains to be developed.' # useful constants num_vars = sum(var_dim for var_dim in var_dicts[0].values()) inf = root.lp.getCoinInfinity() # set infinite lower/upper bounds to 0 so they don't create numerical issues in constraints lb = {idx: CyLPArray([val if val > -inf else 0 for val in n.lp.variablesLower]) for idx, n in disjunctive_nodes.items()} # adjusted lower bound ub = {idx: CyLPArray([val if val < inf else 0 for val in n.lp.variablesUpper]) for idx, n in disjunctive_nodes.items()} # adjusted upper bound # set corresponding variables in cglp to 0 to reflect there is no bound # i.e. this variable should not exist in cglp wb = {idx: CyLPArray([inf if val > -inf else 0 for val in n.lp.variablesLower]) for idx, n in disjunctive_nodes.items()} # w bounds - variable on lb constraints vb = {idx: CyLPArray([inf if val < inf else 0 for val in n.lp.variablesUpper]) for idx, n in disjunctive_nodes.items()} # v bounds - variable on ub constraints # instantiate LP cglp = CyClpSimplex() cglp.logLevel = 0 # quiet output when resolving # declare variables (what to do with case when we have degenerate constraint) pi = cglp.addVariable('pi', num_vars) pi0 = cglp.addVariable('pi0', 1) u = {idx: cglp.addVariable(f'u_{idx}', n.lp.nConstraints) for idx, n in disjunctive_nodes.items()} w = {idx: cglp.addVariable(f'w_{idx}', n.lp.nVariables) for idx, n in disjunctive_nodes.items()} v = {idx: cglp.addVariable(f'v_{idx}', n.lp.nVariables) for idx, n in disjunctive_nodes.items()} # bound them for idx in disjunctive_nodes: cglp += u[idx] >= 0 cglp += 0 <= w[idx] <= wb[idx] cglp += 0 <= v[idx] <= vb[idx] # add constraints for i, n in disjunctive_nodes.items(): # (pi, pi0) must be valid for each disjunctive term's LP relaxation cglp += 0 >= -pi + n.lp.coefMatrix.T * u[i] + \ np.matrix(np.eye(num_vars)) * w[i] - np.matrix(np.eye(num_vars)) * v[i] cglp += 0 <= -pi0 + CyLPArray(n.lp.constraintsLower) * u[i] + \ lb[i] * w[i] - ub[i] * v[i] # normalize variables so they don't grow arbitrarily cglp += sum(var.sum() for var_dict in [u, w, v] for var in var_dict.values()) == 1 # set objective: find the deepest cut # since pi * x >= pi0 for all x in disjunction, we want min pi * x_star - pi0 cglp.objective = CyLPArray(root.solution) * pi - pi0 # solve cglp.primal(startFinishOptions='x') assert cglp.getStatusCode() == 0, 'we should get optimal solution' assert cglp.objectiveValue <= 0, 'pi * x >= pi0 -> pi * x - pi0 >= 0 -> ' \ 'negative objective at x^* since it gets cut off' # get solution return cglp.primalVariableSolution['pi'], cglp.primalVariableSolution['pi0']
def _bound_dual(self, cur_lp: CyClpSimplex) -> CyClpSimplex: """ Place a bound on each index of the dual variable associated with the constraints of this node's LP relaxation and resolve. We do this by adding to each constraint i the slack variable 's_i' in the node's LP relaxation, and we give each new variable a large, positive coefficient in the objective. By duality, we get the desired dual LP constraints. Therefore, for nodes with infeasible primal LP relaxations and unbounded dual LP relaxations, resolving gives us a finite (albeit very large) dual solution, which can be used to parametrically lower bound the objective value of this node as we change its right hand side. :param cur_lp: the CyClpSimplex instance for which we want to bound its dual solution and resolve :return: A CyClpSimplex instance representing the same model as input, with the additions prescribed in the method description """ # we add slack at end so user does not have to worry about adding to each node they create # and so we don't have to go back and update needlessly assert isinstance(cur_lp, CyClpSimplex), 'must give CyClpSimplex instance' for i, constr in enumerate(cur_lp.constraints): assert f's_{i}' not in [v.name for v in cur_lp.variables], \ f"variable 's_{i}' is a reserved name. please name your variable something else" # cylp lacks (a documented) way to add a column, so rebuild the LP :[ new_lp = CyClpSimplex() new_lp.logLevel = 0 # quiet output when resolving # recreate variables var_map = {v: new_lp.addVariable(v.name, v.dim) for v in cur_lp.variables} # bound them for orig_v, new_v in var_map.items(): new_lp += CyLPArray(orig_v.lower) <= new_v <= CyLPArray(orig_v.upper) # re-add constraints, with slacks this time s = {} for i, constr in enumerate(cur_lp.constraints): s[i] = new_lp.addVariable(f's_{i}', constr.nRows) new_lp += s[i] >= CyLPArray(np.zeros(constr.nRows)) new_lp += CyLPArray(constr.lower) <= \ sum(constr.varCoefs[v] * var_map[v] for v in constr.variables) \ + np.matrix(np.identity(constr.nRows))*s[i] <= CyLPArray(constr.upper) # set objective new_lp.objective = sum( CyLPArray(cur_lp.objectiveCoefficients[orig_v.indices]) * new_v for orig_v, new_v in var_map.items() ) + sum(self._M * v.sum() for v in s.values()) # warm start orig_var_status, orig_slack_status = cur_lp.getBasisStatus() # each s_i at lower bound of 0 when added - status 3 np.concatenate((orig_var_status, np.ones(sum(v.dim for v in s.values()))*3)) new_lp.setBasisStatus(orig_var_status, orig_slack_status) # rerun and reassign new_lp.dual(startFinishOptions='x') return new_lp
def LP_solver_cylp(A_Matrix, B_vectors, weights, really_verbose=False): """ Solve the Linear Programming problem given in Giangrande et al, 2012 using the CyLP module. Parameters ---------- A_Matrix : matrix Row augmented A matrix, see :py:func:`construct_A_matrix` B_vectors : matrix Matrix containing B vectors, see :py:func:`construct_B_vectors` weights : array Weights. really_verbose : bool True to print CLP messaging. False to suppress. Returns ------- soln : array Solution to LP problem. See Also -------- LP_solver_cvxopt : Solve LP problem using the CVXOPT module. LP_solver_pyglpk : Solve LP problem using the PyGLPK module. """ from cylp.cy.CyClpSimplex import CyClpSimplex from cylp.py.modeling.CyLPModel import CyLPModel, CyLPArray n_gates = weights.shape[1] // 2 n_rays = B_vectors.shape[0] soln = np.zeros([n_rays, n_gates]) # Create CyLPModel and initialize it model = CyLPModel() G = np.matrix(A_Matrix) h = CyLPArray(np.empty(B_vectors.shape[1])) x = model.addVariable('x', G.shape[1]) model.addConstraint(G * x >= h) #c = CyLPArray(np.empty(weights.shape[1])) c = CyLPArray(np.squeeze(weights[0])) model.objective = c * x # import model in solver s = CyClpSimplex(model) # disable logging if not really_verbose: s.logLevel = 0 for raynum in range(n_rays): # set new B_vector values for actual ray s.setRowLowerArray(np.squeeze(np.asarray(B_vectors[raynum]))) # set new weights (objectives) for actual ray #s.setObjectiveArray(np.squeeze(np.asarray(weights[raynum]))) # solve with dual method, it is faster s.dual() # extract primal solution soln[raynum, :] = s.primalVariableSolution['x'][n_gates: 2 * n_gates] # apply smoothing filter on a per scan basis soln = smooth_and_trim_scan(soln, window_len=5, window='sg_smooth') return soln
def LP_solver_cylp(A_Matrix, B_vectors, weights, really_verbose=False): """ Solve the Linear Programming problem given in Giangrande et al, 2012 using the CyLP module. Parameters ---------- A_Matrix : matrix Row augmented A matrix, see :py:func:`construct_A_matrix` B_vectors : matrix Matrix containing B vectors, see :py:func:`construct_B_vectors` weights : array Weights. really_verbose : bool True to print CLP messaging. False to suppress. Returns ------- soln : array Solution to LP problem. See Also -------- LP_solver_cvxopt : Solve LP problem using the CVXOPT module. LP_solver_pyglpk : Solve LP problem using the PyGLPK module. """ from cylp.cy.CyClpSimplex import CyClpSimplex from cylp.py.modeling.CyLPModel import CyLPModel, CyLPArray n_gates = weights.shape[1] // 2 n_rays = B_vectors.shape[0] soln = np.zeros([n_rays, n_gates]) # Create CyLPModel and initialize it model = CyLPModel() G = np.matrix(A_Matrix) h = CyLPArray(np.empty(B_vectors.shape[1])) x = model.addVariable('x', G.shape[1]) model.addConstraint(G * x >= h) #c = CyLPArray(np.empty(weights.shape[1])) c = CyLPArray(np.squeeze(weights[0])) model.objective = c * x # import model in solver s = CyClpSimplex(model) # disable logging if not really_verbose: s.logLevel = 0 for raynum in range(n_rays): # set new B_vector values for actual ray s.setRowLowerArray(np.squeeze(np.asarray(B_vectors[raynum]))) # set new weights (objectives) for actual ray #s.setObjectiveArray(np.squeeze(np.asarray(weights[raynum]))) # solve with dual method, it is faster s.dual() # extract primal solution soln[raynum, :] = s.primalVariableSolution['x'][n_gates:2 * n_gates] # apply smoothing filter on a per scan basis soln = smooth_and_trim_scan(soln, window_len=5, window='sg_smooth') return soln
def BranchAndBoundCylp(T, CONSTRAINTS, VARIABLES, OBJ, MAT, RHS, branch_strategy=MOST_FRACTIONAL, search_strategy=DEPTH_FIRST, complete_enumeration=False, display_interval=None, binary_vars=True): if T.get_layout() == 'dot2tex': cluster_attrs = {'name':'Key', 'label':r'\text{Key}', 'fontsize':'12'} T.add_node('C', label = r'\text{Candidate}', style = 'filled', color = 'yellow', fillcolor = 'yellow') T.add_node('I', label = r'\text{Infeasible}', style = 'filled', color = 'orange', fillcolor = 'orange') T.add_node('S', label = r'\text{Solution}', style = 'filled', color = 'lightblue', fillcolor = 'lightblue') T.add_node('P', label = r'\text{Pruned}', style = 'filled', color = 'red', fillcolor = 'red') T.add_node('PC', label = r'\text{Pruned}$\\ $\text{Candidate}', style = 'filled', color = 'red', fillcolor = 'yellow') else: cluster_attrs = {'name':'Key', 'label':'Key', 'fontsize':'12'} T.add_node('C', label = 'Candidate', style = 'filled', color = 'yellow', fillcolor = 'yellow') T.add_node('I', label = 'Infeasible', style = 'filled', color = 'orange', fillcolor = 'orange') T.add_node('S', label = 'Solution', style = 'filled', color = 'lightblue', fillcolor = 'lightblue') T.add_node('P', label = 'Pruned', style = 'filled', color = 'red', fillcolor = 'red') T.add_node('PC', label = 'Pruned \n Candidate', style = 'filled', color = 'red', fillcolor = 'yellow') T.add_edge('C', 'I', style = 'invisible', arrowhead = 'none') T.add_edge('I', 'S', style = 'invisible', arrowhead = 'none') T.add_edge('S', 'P', style = 'invisible', arrowhead = 'none') T.add_edge('P', 'PC', style = 'invisible', arrowhead = 'none') T.create_cluster(['C', 'I', 'S', 'P', 'PC'], cluster_attrs) # Change to CyLP format cyOBJ = CyLPArray([-val for val in OBJ.values()]) # CyLP takes min cyMAT = np.matrix([MAT[v] for v in VARIABLES]).T cyRHS = CyLPArray(RHS) # The initial lower bound LB = -INFINITY # The number of LP's solved, and the number of nodes solved node_count = 1 iter_count = 0 lp_count = 0 numCons = len(CONSTRAINTS) numVars = len(VARIABLES) # List of incumbent solution variable values opt = dict([(i, 0) for i in VARIABLES]) pseudo_u = dict((i, (OBJ[i], 0)) for i in VARIABLES) pseudo_d = dict((i, (OBJ[i], 0)) for i in VARIABLES) print("===========================================") print("Starting Branch and Bound") if branch_strategy == MOST_FRACTIONAL: print("Most fractional variable") elif branch_strategy == FIXED_BRANCHING: print("Fixed order") elif branch_strategy == PSEUDOCOST_BRANCHING: print("Pseudocost brancing") else: print("Unknown branching strategy %s" %branch_strategy) if search_strategy == DEPTH_FIRST: print("Depth first search strategy") elif search_strategy == BEST_FIRST: print("Best first search strategy") else: print("Unknown search strategy %s" %search_strategy) print("===========================================") # List of candidate nodes Q = PriorityQueue() # The current tree depth cur_depth = 0 cur_index = 0 # Timer timer = time.time() Q.push(0, -INFINITY, (0, None, None, None, None, None, None)) # Branch and Bound Loop while not Q.isEmpty(): infeasible = False integer_solution = False (cur_index, parent, relax, branch_var, branch_var_value, sense, rhs) = Q.pop() if cur_index is not 0: cur_depth = T.get_node_attr(parent, 'level') + 1 else: cur_depth = 0 print("") print("----------------------------------------------------") print("") if LB > -INFINITY: print("Node: %s, Depth: %s, LB: %s" %(cur_index,cur_depth,LB)) else: print("Node: %s, Depth: %s, LB: %s" %(cur_index,cur_depth,"None")) if relax is not None and relax <= LB: print("Node pruned immediately by bound") T.set_node_attr(parent, 'color', 'red') continue #==================================== # LP Relaxation #==================================== # Compute lower bound by LP relaxation prob = CyLPModel() # CyLP do not allow change variable object without model if binary_vars: var = prob.addVariable('x', dim=len(VARIABLES)) prob += 0 <= var <= 1 else: var = prob.addVariable('x', dim=len(VARIABLES)) prob += 0 <= var # To avoid random generated problems be unbounded prob += cyMAT * var <= RHS prob.objective = cyOBJ * var s = CyClpSimplex(prob) # Fix all prescribed variables branch_vars = [] if cur_index is not 0: sys.stdout.write("Branching variables: ") branch_vars.append(branch_var) if sense == '>=': prob += var[int(branch_var[1:])] >= rhs # slice the name for index else: prob += var[int(branch_var[1:])] <= rhs # slice the name for index print(branch_var, end=' ') pred = parent while not str(pred) == '0': pred_branch_var = T.get_node_attr(pred, 'branch_var') pred_rhs = T.get_node_attr(pred, 'rhs') pred_sense = T.get_node_attr(pred, 'sense') if pred_sense == '<=': prob += var[int(pred_branch_var[1:])] <= pred_rhs else: prob += var[int(pred_branch_var[1:])] >= pred_rhs print(pred_branch_var, end=' ') branch_vars.append(pred_branch_var) pred = T.get_node_attr(pred, 'parent') print() # Solve the LP relaxation s = CyClpSimplex(prob) s.initialSolve() lp_count = lp_count +1 if (s.getStatusCode() < 0): print('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%') print(lp_count) print(s.getStatusCode()) print(s.primal()) print(s.objectiveValue) print(s.primalVariableSolution) print('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%') # Check infeasibility #-1 - unknown e.g. before solve or if postSolve says not optimal #0 - optimal #1 - primal infeasible #2 - dual infeasible #3 - stopped on iterations or time #4 - stopped due to errors #5 - stopped by event handler (virtual int ClpEventHandler::event()) infeasible = (s.getStatusCode() in [-1,1,3,4,5]) # Print status if infeasible: print("LP Solved, status: Infeasible") else: print("LP Solved, status: %s, obj: %s" %(s.getStatusString(),-s.objectiveValue)) if(s.getStatusCode() == 0): relax = -s.objectiveValue # Update pseudocost if branch_var != None: if sense == '<=': pseudo_d[branch_var] = ( old_div((pseudo_d[branch_var][0]*pseudo_d[branch_var][1] + old_div((T.get_node_attr(parent, 'obj') - relax), (branch_var_value - rhs))),(pseudo_d[branch_var][1]+1)), pseudo_d[branch_var][1]+1) else: pseudo_u[branch_var] = ( old_div((pseudo_u[branch_var][0]*pseudo_d[branch_var][1] + old_div((T.get_node_attr(parent, 'obj') - relax), (rhs - branch_var_value))),(pseudo_u[branch_var][1]+1)), pseudo_u[branch_var][1]+1) var_values = dict([(i, s.primalVariableSolution['x'][int(i[1:])]) for i in VARIABLES]) integer_solution = 1 for i in VARIABLES: if (abs(round(var_values[i]) - var_values[i]) > .001): integer_solution = 0 break # Determine integer_infeasibility_count and # Integer_infeasibility_sum for scatterplot and such integer_infeasibility_count = 0 integer_infeasibility_sum = 0.0 for i in VARIABLES: if (var_values[i] not in set([0,1])): integer_infeasibility_count += 1 integer_infeasibility_sum += min([var_values[i], 1.0-var_values[i]]) if (integer_solution and relax>LB): LB = relax for i in VARIABLES: # These two have different data structures first one #list, second one dictionary opt[i] = var_values[i] print("New best solution found, objective: %s" %relax) for i in VARIABLES: if var_values[i] > 0: print("%s = %s" %(i, var_values[i])) elif (integer_solution and relax<=LB): print("New integer solution found, objective: %s" %relax) for i in VARIABLES: if var_values[i] > 0: print("%s = %s" %(i, var_values[i])) else: print("Fractional solution:") for i in VARIABLES: if var_values[i] > 0: print("%s = %s" %(i, var_values[i])) #For complete enumeration if complete_enumeration: relax = LB - 1 else: relax = INFINITY if integer_solution: print("Integer solution") BBstatus = 'S' status = 'integer' color = 'lightblue' elif infeasible: print("Infeasible node") BBstatus = 'I' status = 'infeasible' color = 'orange' elif not complete_enumeration and relax <= LB: print("Node pruned by bound (obj: %s, UB: %s)" %(relax,LB)) BBstatus = 'P' status = 'fathomed' color = 'red' elif cur_depth >= numVars : print("Reached a leaf") BBstatus = 'fathomed' status = 'L' else: BBstatus = 'C' status = 'candidate' color = 'yellow' if BBstatus is 'I': if T.get_layout() == 'dot2tex': label = '\text{I}' else: label = 'I' else: label = "%.1f"%relax if iter_count == 0: if status is not 'candidate': integer_infeasibility_count = None integer_infeasibility_sum = None if status is 'fathomed': if T._incumbent_value is None: print('WARNING: Encountered "fathom" line before '+\ 'first incumbent.') T.AddOrUpdateNode(0, None, None, 'candidate', relax, integer_infeasibility_count, integer_infeasibility_sum, label = label, obj = relax, color = color, style = 'filled', fillcolor = color) if status is 'integer': T._previous_incumbent_value = T._incumbent_value T._incumbent_value = relax T._incumbent_parent = -1 T._new_integer_solution = True # #Currently broken # if ETREE_INSTALLED and T.attr['display'] == 'svg': # T.write_as_svg(filename = "node%d" % iter_count, # nextfile = "node%d" % (iter_count + 1), # highlight = cur_index) else: _direction = {'<=':'L', '>=':'R'} if status is 'infeasible': integer_infeasibility_count = T.get_node_attr(parent, 'integer_infeasibility_count') integer_infeasibility_sum = T.get_node_attr(parent, 'integer_infeasibility_sum') relax = T.get_node_attr(parent, 'lp_bound') elif status is 'fathomed': if T._incumbent_value is None: print('WARNING: Encountered "fathom" line before'+\ ' first incumbent.') print(' This may indicate an error in the input file.') elif status is 'integer': integer_infeasibility_count = None integer_infeasibility_sum = None T.AddOrUpdateNode(cur_index, parent, _direction[sense],\ status, relax,\ integer_infeasibility_count,\ integer_infeasibility_sum,\ branch_var = branch_var,\ branch_var_value = var_values[branch_var],\ sense = sense, rhs = rhs, obj = relax,\ color = color, style = 'filled',\ label = label, fillcolor = color) if status is 'integer': T._previous_incumbent_value = T._incumbent_value T._incumbent_value = relax T._incumbent_parent = parent T._new_integer_solution = True # Currently Broken # if ETREE_INSTALLED and T.attr['display'] == 'svg': # T.write_as_svg(filename = "node%d" % iter_count, # prevfile = "node%d" % (iter_count - 1), # nextfile = "node%d" % (iter_count + 1), # highlight = cur_index) if T.get_layout() == 'dot2tex': _dot2tex_label = {'>=':' \geq ', '<=':' \leq '} T.set_edge_attr(parent, cur_index, 'label',\ str(branch_var) + _dot2tex_label[sense] +\ str(rhs)) else: T.set_edge_attr(parent, cur_index, 'label',\ str(branch_var) + sense + str(rhs)) iter_count += 1 if BBstatus == 'C': # Branching: # Choose a variable for branching branching_var = None if branch_strategy == FIXED_BRANCHING: #fixed order for i in VARIABLES: frac = min(s.primalVariableSolution['x'][int(i[1:])]-math.floor(s.primalVariableSolution['x'][int(i[1:])]),\ math.ceil(s.primalVariableSolution['x'][int(i[1:])]) - s.primalVariableSolution['x'][int(i[1:])]) if (frac > 0): min_frac = frac branching_var = i # TODO(aykut): understand this break break elif branch_strategy == MOST_FRACTIONAL: #most fractional variable min_frac = -1 for i in VARIABLES: frac = min(s.primalVariableSolution['x'][int(i[1:])]-math.floor(s.primalVariableSolution['x'][int(i[1:])]),\ math.ceil(s.primalVariableSolution['x'][int(i[1:])])- s.primalVariableSolution['x'][int(i[1:])]) if (frac> min_frac): min_frac = frac branching_var = i elif branch_strategy == PSEUDOCOST_BRANCHING: scores = {} for i in VARIABLES: # find the fractional solutions if (s.primalVariableSolution['x'][int(i[1:])] - math.floor(s.primalVariableSolution['x'][int(i[1:])])) != 0: scores[i] = min(pseudo_u[i][0]*(1-s.primalVariableSolution['x'][int(i[1:])]),\ pseudo_d[i][0]*s.primalVariableSolution['x'][int(i[1:])]) # sort the dictionary by value branching_var = sorted(list(scores.items()), key=lambda x : x[1])[-1][0] else: print("Unknown branching strategy %s" %branch_strategy) exit() if branching_var is not None: print("Branching on variable %s" %branching_var) #Create new nodes if search_strategy == DEPTH_FIRST: priority = (-cur_depth - 1, -cur_depth - 1) elif search_strategy == BEST_FIRST: priority = (-relax, -relax) elif search_strategy == BEST_ESTIMATE: priority = (-relax - pseudo_d[branching_var][0]*\ (math.floor(s.primalVariableSolution['x'][int(branching_var[1:])]) -\ s.primalVariableSolution['x'][int(branching_var[1:])]),\ -relax + pseudo_u[branching_var][0]*\ (math.ceil(s.primalVariableSolution['x'][int(branching_var[1:])]) -\ s.primalVariableSolution['x'][int(branching_var[1:])])) node_count += 1 Q.push(node_count, priority[0], (node_count, cur_index, relax, branching_var, var_values[branching_var], '<=', math.floor(s.primalVariableSolution['x'][int(branching_var[1:])]))) node_count += 1 Q.push(node_count, priority[1], (node_count, cur_index, relax, branching_var, var_values[branching_var], '>=', math.ceil(s.primalVariableSolution['x'][int(branching_var[1:])]))) T.set_node_attr(cur_index, color, 'green') if T.root is not None and display_interval is not None and\ iter_count%display_interval == 0: T.display(count=iter_count) timer = int(math.ceil((time.time()-timer)*1000)) print("") print("===========================================") print("Branch and bound completed in %sms" %timer) print("Strategy: %s" %branch_strategy) if complete_enumeration: print("Complete enumeration") print("%s nodes visited " %node_count) print("%s LP's solved" %lp_count) print("===========================================") print("Optimal solution") #print optimal solution for i in sorted(VARIABLES): if opt[i] > 0: print("%s = %s" %(i, opt[i])) print("Objective function value") print(LB) print("===========================================") if T.attr['display'] is not 'off': T.display(count=iter_count) T._lp_count = lp_count return LB,opt
def BranchAndBound(T, CONSTRAINTS, VARIABLES, OBJ, MAT, RHS, branch_strategy=MOST_FRACTIONAL, search_strategy=DEPTH_FIRST, complete_enumeration=False, display_interval=None, binary_vars=True, solver='dynamic', rel_param=(4, 3, 1 / 6, 5), more_return=False ): """ solver: dynamic - initialSolve primalSimplex - initialPrimalSolve dualSimplex - initialDualSolve Parameter Tuple for Reliability Branching rel_param = (eta_rel,gamma,mu,lambda): eta_rel = 0 - psesudocost branching eta_rel = 1 - psesudocost branching with strong branching initialization eta_rel = 4,8 - best performing reliability branching rules eta_rel = inifity - strong branching gamma - max number of simplex iterations in score calculation mu - score factor, a number between 0 and 1. Paper uses 1/6 lambda - if max score is not updated for lambda consecutive iterations, stop. more_return: False - return maximizer and max True - also return a dict of stats(time, tree size, LP solved) """ ACTUAL_BRANCH_STRATEGY = branch_strategy # reliability branching parameters eta_rel, gamma, mu, lam = rel_param # hybrid branching parameters total_num_pivot = average_num_pivot = 0 # translate problems into cylp format cyOBJ = CyLPArray([-val for val in OBJ.values()]) cyMAT = np.matrix([MAT[v] for v in VARIABLES]).T cyRHS = CyLPArray(RHS) OBJ = cyOBJ MAT = cyMAT RHS = cyRHS if T.get_layout() == 'dot2tex': cluster_attrs = {'name': 'Key', 'label': r'\text{Key}', 'fontsize': '12'} T.add_node('C', label=r'\text{Candidate}', style='filled', color='yellow', fillcolor='yellow') T.add_node('I', label=r'\text{Infeasible}', style='filled', color='orange', fillcolor='orange') T.add_node('S', label=r'\text{Solution}', style='filled', color='lightblue', fillcolor='lightblue') T.add_node('P', label=r'\text{Pruned}', style='filled', color='red', fillcolor='red') T.add_node('PC', label=r'\text{Pruned}$\\ $\text{Candidate}', style='filled', color='red', fillcolor='yellow') else: cluster_attrs = {'name': 'Key', 'label': 'Key', 'fontsize': '12'} T.add_node('C', label='Candidate', style='filled', color='yellow', fillcolor='yellow') T.add_node('I', label='Infeasible', style='filled', color='orange', fillcolor='orange') T.add_node('S', label='Solution', style='filled', color='lightblue', fillcolor='lightblue') T.add_node('P', label='Pruned', style='filled', color='red', fillcolor='red') T.add_node('PC', label='Pruned \n Candidate', style='filled', color='red', fillcolor='yellow') T.add_edge('C', 'I', style='invisible', arrowhead='none') T.add_edge('I', 'S', style='invisible', arrowhead='none') T.add_edge('S', 'P', style='invisible', arrowhead='none') T.add_edge('P', 'PC', style='invisible', arrowhead='none') T.create_cluster(['C', 'I', 'S', 'P', 'PC'], cluster_attrs) # The initial lower bound LB = -INFINITY # The number of LP's solved, and the number of nodes solved node_count = 1 iter_count = 0 lp_count = 0 # The problems that are fully solved during # Reliability and Hybrid branching is also couned here # For reliability branching half_solved = 0 # record number problems been halfly solved when calculate scores full_solved = 0 # record number problems been fully solved when calculate scores numVars = len(VARIABLES) # List of incumbent solution variable values opt = dict([(i, 0) for i in range(len(VARIABLES))]) pseudo_u = dict((i, (-OBJ[i], 0)) for i in range(len(VARIABLES))) pseudo_d = dict((i, (-OBJ[i], 0)) for i in range(len(VARIABLES))) print("===========================================") print("Starting Branch and Bound") if branch_strategy == MOST_FRACTIONAL: print("Most fractional variable") elif branch_strategy == FIXED_BRANCHING: print("Fixed order") elif branch_strategy == PSEUDOCOST_BRANCHING: print("Pseudocost brancing") elif branch_strategy == RELIABILITY_BRANCHING: print("Reliability branching") elif branch_strategy == HYBRID: print('Hybrid strong/pseduocost branching') else: print("Unknown branching strategy %s" % branch_strategy) if search_strategy == DEPTH_FIRST: print("Depth first search strategy") elif search_strategy == BEST_FIRST: print("Best first search strategy") elif search_strategy == BEST_ESTIMATE: print("Best estimate search strategy") else: print("Unknown search strategy %s" % search_strategy) print("===========================================") # List of candidate nodes Q = PriorityQueue() # The current tree depth cur_depth = 0 cur_index = 0 # Timer timer = time.time() Q.push(0, -INFINITY, (0, None, None, None, None, None, None)) # Branch and Bound Loop while not Q.isEmpty(): # maximum allowed strong branch performed if branch_strategy == HYBRID and cur_depth > max(int(len(VARIABLES) * 0.2), 5): branch_strategy = PSEUDOCOST_BRANCHING print("Switch from strong branch to psedocost branch") infeasible = False integer_solution = False (cur_index, parent, relax, branch_var, branch_var_value, sense, rhs) = Q.pop() if cur_index is not 0: cur_depth = T.get_node_attr(parent, 'level') + 1 else: cur_depth = 0 print("") print("----------------------------------------------------") print("") if LB > -INFINITY: print("Node: %s, Depth: %s, LB: %s" % (cur_index, cur_depth, LB)) else: print("Node: %s, Depth: %s, LB: %s" % (cur_index, cur_depth, "None")) if relax is not None and relax <= LB: print("Node pruned immediately by bound") T.set_node_attr(parent, 'color', 'red') continue # ==================================== # LP Relaxation # ==================================== # Compute lower bound by LP relaxation prob = CyLPModel() if binary_vars: x = prob.addVariable('x', dim=len(VARIABLES)) prob += 0 <= x <= 1 else: x = prob.addVariable('x', dim=len(VARIABLES)) prob.objective = OBJ * x prob += MAT * x <= RHS # Fix all prescribed variables branch_vars = [] if cur_index is not 0: sys.stdout.write("Branching variables: ") branch_vars.append(branch_var) if sense == '>=': prob += x[branch_var] >= rhs else: prob += x[branch_var] <= rhs print('x_{}'.format(branch_var), end=' ') pred = parent while not str(pred) == '0': pred_branch_var = T.get_node_attr(pred, 'branch_var') pred_rhs = T.get_node_attr(pred, 'rhs') pred_sense = T.get_node_attr(pred, 'sense') if pred_sense == '<=': prob += x[pred_branch_var] <= pred_rhs else: prob += x[pred_branch_var] >= pred_rhs print(pred_branch_var, end=' ') branch_vars.append(pred_branch_var) pred = T.get_node_attr(pred, 'parent') print() # Solve the LP relaxation s = CyClpSimplex(prob) if solver == 'primalSimplex': s.initialPrimalSolve() elif solver == 'dualSimplex': s.initialDualSolve() else: s.initialSolve() lp_count = lp_count + 1 total_num_pivot += s.iteration average_num_pivot = total_num_pivot / lp_count # Check infeasibility # -1 - unknown e.g. before solve or if postSolve says not optimal # 0 - optimal # 1 - primal infeasible # 2 - dual infeasible # 3 - stopped on iterations or time # 4 - stopped due to errors # 5 - stopped by event handler (virtual int ClpEventHandler::event()) infeasible = (s.getStatusCode() in [1, 2]) # Print status if infeasible: print("LP Solved, status: Infeasible") else: print("LP Solved, status: %s, obj: %s" % (s.getStatusString(), s.objectiveValue)) if(s.getStatusCode() == 0): relax = -round(s.objectiveValue,7) # Update pseudocost if branch_var != None: if sense == '<=': pseudo_d[branch_var] = ( old_div((pseudo_d[branch_var][0] * pseudo_d[branch_var][1] + old_div((T.get_node_attr(parent, 'obj') - relax), (branch_var_value - rhs))), (pseudo_d[branch_var][1] + 1)), pseudo_d[branch_var][1] + 1) else: pseudo_u[branch_var] = ( old_div((pseudo_u[branch_var][0] * pseudo_d[branch_var][1] + old_div((T.get_node_attr(parent, 'obj') - relax), (rhs - branch_var_value))), (pseudo_u[branch_var][1] + 1)), pseudo_u[branch_var][1] + 1) var_values = dict([(i, round(s.primalVariableSolution['x'][i],7)) for i in range(len(VARIABLES))]) integer_solution = 1 for i in range(len(VARIABLES)): if (abs(round(var_values[i]) - var_values[i]) > .001): integer_solution = 0 break # Determine integer_infeasibility_count and # Integer_infeasibility_sum for scatterplot and such integer_infeasibility_count = 0 integer_infeasibility_sum = 0.0 for i in range(len(VARIABLES)): if (var_values[i] not in set([0, 1])): integer_infeasibility_count += 1 integer_infeasibility_sum += min([var_values[i], 1.0 - var_values[i]]) if (integer_solution and relax > LB): LB = relax for i in range(len(VARIABLES)): # These two have different data structures first one # list, second one dictionary opt[i] = var_values[i] print("New best solution found, objective: %s" % relax) for i in range(len(VARIABLES)): if var_values[i] > 0: print("%s = %s" % (i, var_values[i])) elif (integer_solution and relax <= LB): print("New integer solution found, objective: %s" % relax) for i in range(len(VARIABLES)): if var_values[i] > 0: print("%s = %s" % (i, var_values[i])) else: print("Fractional solution:") for i in range(len(VARIABLES)): if var_values[i] > 0: print("x%s = %s" % (i, var_values[i])) # For complete enumeration if complete_enumeration: relax = LB - 1 else: relax = INFINITY if integer_solution: print("Integer solution") BBstatus = 'S' status = 'integer' color = 'lightblue' elif infeasible: print("Infeasible node") BBstatus = 'I' status = 'infeasible' color = 'orange' elif not complete_enumeration and relax <= LB: print("Node pruned by bound (obj: %s, UB: %s)" % (relax, LB)) BBstatus = 'P' status = 'fathomed' color = 'red' elif cur_depth >= numVars: print("Reached a leaf") BBstatus = 'fathomed' status = 'L' else: BBstatus = 'C' status = 'candidate' color = 'yellow' if BBstatus is 'I': if T.get_layout() == 'dot2tex': label = '\text{I}' else: label = 'I' else: label = "%.1f" % relax if iter_count == 0: if status is not 'candidate': integer_infeasibility_count = None integer_infeasibility_sum = None if status is 'fathomed': if T._incumbent_value is None: print('WARNING: Encountered "fathom" line before ' + 'first incumbent.') T.AddOrUpdateNode(0, None, None, 'candidate', relax, integer_infeasibility_count, integer_infeasibility_sum, label=label, obj=relax, color=color, style='filled', fillcolor=color) if status is 'integer': T._previous_incumbent_value = T._incumbent_value T._incumbent_value = relax T._incumbent_parent = -1 T._new_integer_solution = True # #Currently broken # if ETREE_INSTALLED and T.attr['display'] == 'svg': # T.write_as_svg(filename = "node%d" % iter_count, # nextfile = "node%d" % (iter_count + 1), # highlight = cur_index) else: _direction = {'<=': 'L', '>=': 'R'} if status is 'infeasible': integer_infeasibility_count = T.get_node_attr(parent, 'integer_infeasibility_count') integer_infeasibility_sum = T.get_node_attr(parent, 'integer_infeasibility_sum') relax = T.get_node_attr(parent, 'lp_bound') elif status is 'fathomed': if T._incumbent_value is None: print('WARNING: Encountered "fathom" line before' + ' first incumbent.') print(' This may indicate an error in the input file.') elif status is 'integer': integer_infeasibility_count = None integer_infeasibility_sum = None T.AddOrUpdateNode(cur_index, parent, _direction[sense], status, relax, integer_infeasibility_count, integer_infeasibility_sum, branch_var=branch_var, branch_var_value=var_values[branch_var], sense=sense, rhs=rhs, obj=relax, color=color, style='filled', label=label, fillcolor=color) if status is 'integer': T._previous_incumbent_value = T._incumbent_value T._incumbent_value = relax T._incumbent_parent = parent T._new_integer_solution = True # Currently Broken # if ETREE_INSTALLED and T.attr['display'] == 'svg': # T.write_as_svg(filename = "node%d" % iter_count, # prevfile = "node%d" % (iter_count - 1), # nextfile = "node%d" % (iter_count + 1), # highlight = cur_index) if T.get_layout() == 'dot2tex': _dot2tex_label = {'>=': ' \geq ', '<=': ' \leq '} T.set_edge_attr(parent, cur_index, 'label', str(branch_var) + _dot2tex_label[sense] + str(rhs)) else: T.set_edge_attr(parent, cur_index, 'label', str(branch_var) + sense + str(rhs)) iter_count += 1 if BBstatus == 'C': # Branching: # Choose a variable for branching branching_var = None if branch_strategy == FIXED_BRANCHING: # fixed order for i in range(len(VARIABLES)): frac = min(var_values[i] - math.floor(var_values[i]), math.ceil(var_values[i]) - var_values[i]) if (frac > 0): min_frac = frac branching_var = i # TODO(aykut): understand this break break elif branch_strategy == MOST_FRACTIONAL: # most fractional variable min_frac = -1 for i in range(len(VARIABLES)): frac = min(var_values[i] - math.floor(var_values[i]), math.ceil(var_values[i]) - var_values[i]) if (frac > min_frac): min_frac = frac branching_var = i elif branch_strategy == PSEUDOCOST_BRANCHING: scores = {} for i in range(len(VARIABLES)): # find the fractional solutions if abs(var_values[i] - math.floor(var_values[i])) > 1e-8: scores[i] = min(pseudo_u[i][0] * (math.ceil(var_values[i]) - var_values[i]), pseudo_d[i][0] * (var_values[i] - math.floor(var_values[i]))) # sort the dictionary by value branching_var = sorted(list(scores.items()), key=lambda x: x[1])[-1][0] elif branch_strategy == RELIABILITY_BRANCHING: # Calculating Scores # The algorithm in paper is different from the one in Grumpy # I will try to use paper notations scores = {} for i in range(len(VARIABLES)): # find the fractional solutions if abs(var_values[i] - math.floor(var_values[i])) > 1e-8: qp = pseudo_u[i][0] * (math.ceil(var_values[i]) - var_values[i]) # q^+ qm = pseudo_d[i][0] * (var_values[i] - math.floor(var_values[i])) # q^^- scores[i] = (1 - mu) * min(qm, qp) + mu * max(qm, qp) # sort the dictionary by value candidate_vars = [en[0] for en in sorted(list(scores.items()), key=lambda x: x[1], reverse=True)] no_change = 0 # number of iterations that maximum of scores is not changed smax = scores[candidate_vars[0]] # current maximum of scores for i in candidate_vars: if min(pseudo_d[i][1], pseudo_u[i][1]) < eta_rel: qp = pseudo_u[i][0] * (math.ceil(var_values[i]) - var_values[i]) # q^+ qm = pseudo_d[i][0] * (var_values[i] - math.floor(var_values[i])) # q^^- # left subproblem/down direction s_left = CyClpSimplex(prob) s_left += x[i] <= math.floor(var_values[i]) s_left.maxNumIteration = gamma # solve for fixed number of iterations s_left.dual() if s_left.getStatusCode() == 0: qm = relax + s_left.objectiveValue # use a more reliable source to update q^- full_solved = full_solved + 1 lp_count = lp_count + 1 # If the LP is fully solved, counter plus one elif s_left.getStatusCode() == 3: qm = relax + s_left.objectiveValue # use a more reliable source to update q^- half_solved = half_solved + 1 # right subproblem/up direction s_right = CyClpSimplex(prob) s_right += x[i] >= math.ceil(var_values[i]) s_right.maxNumIteration = gamma # solve for fixed number of iterations s_right.dual() if s_right.getStatusCode() == 0: qp = relax + s_right.objectiveValue # use a more reliable source to update q^+ full_solved = full_solved + 1 lp_count = lp_count + 1 # If the LP is fully solved, counter plus one elif s_right.getStatusCode() == 3: qp = relax + s_right.objectiveValue # use a more reliable source to update q^+ half_solved = half_solved + 1 scores[i] = (1 - mu) * min(qm, qp) + mu * max(qm, qp) if (smax == scores[i]): no_change += 1 elif (smax <= scores[i]): no_change = 0 smax = scores[i] else: no_change = 0 if no_change >= lam: break branching_var = sorted(list(scores.items()), key=lambda x: x[1])[-1][0] elif branch_strategy == HYBRID: scores = {} for i in range(len(VARIABLES)): # find the fractional solutions if abs(var_values[i] - math.floor(var_values[i])) > 1e-8: scores[i] = min(pseudo_u[i][0] * (math.ceil(var_values[i]) - var_values[i]), pseudo_d[i][0] * (var_values[i] - math.floor(var_values[i]))) candidate_vars = [en[0] for en in sorted(list(scores.items()), key=lambda x: x[1], reverse=True)] restricted_candidate_vars = candidate_vars[:max(1, int(0.5 * len(candidate_vars)))] # print("Subset of candidate variables:") # print(restricted_candidate_vars) best_progress = 0 branch_candidate = None for i in restricted_candidate_vars: s = CyClpSimplex(prob) s += math.floor(var_values[i]) <= x[i] <= math.ceil(var_values[i]) s.maxNumIteration = average_num_pivot * 2 s.dual() if s.getStatusCode() == 0: lp_count += 1 progress = relax - (-s.objectiveValue) if (progress - best_progress) > 1e-8: branch_candidate = i best_progress = progress if branch_candidate is None: branch_candidate = restricted_candidate_vars[0] branching_var = branch_candidate else: print("Unknown branching strategy %s" % branch_strategy) exit() if branching_var is not None: print("Branching on variable %s" % branching_var) # Create new nodes if search_strategy == DEPTH_FIRST: priority = (-cur_depth - 1, -cur_depth - 1) elif search_strategy == BEST_FIRST: priority = (-relax, -relax) elif search_strategy == BEST_ESTIMATE: priority = (-relax - pseudo_d[branching_var][0] * (math.floor(var_values[branching_var]) - var_values[branching_var]), -relax + pseudo_u[branching_var][0] * (math.ceil(var_values[branching_var]) - var_values[branching_var])) node_count += 1 Q.push(node_count, priority[0], (node_count, cur_index, relax, branching_var, var_values[branching_var], '<=', math.floor(var_values[branching_var]))) node_count += 1 Q.push(node_count, priority[1], (node_count, cur_index, relax, branching_var, var_values[branching_var], '>=', math.ceil(var_values[branching_var]))) T.set_node_attr(cur_index, color, 'green') if T.root is not None and display_interval is not None and\ iter_count % display_interval == 0: T.display(count=iter_count) timer = int(math.ceil((time.time() - timer) * 1000)) print("") print("===========================================") print("Branch and bound completed in %sms" % timer) print("Strategy: %s" % ACTUAL_BRANCH_STRATEGY) if complete_enumeration: print("Complete enumeration") print("%s nodes visited " % node_count) print("%s LP's solved" % lp_count) print("===========================================") print("Optimal solution") # print optimal solution for i in range(len(VARIABLES)): if opt[i] > 0: print("x%s = %s" % (i, opt[i])) print("Objective function value") print(LB) print("===========================================") if T.attr['display'] is not 'off': T.display(count=iter_count) T._lp_count = lp_count if more_return: stat = {'Time': timer, 'Size': node_count, 'LP Solved': lp_count} if branch_strategy == RELIABILITY_BRANCHING: stat['LP Solved for Bounds'] = lp_count - full_solved stat['Halfly Solved'] = half_solved stat['Fully Solved'] = full_solved return opt, LB, stat return opt, LB