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 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 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 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