def test_find_strong_disjunctive_cut(self): bb = BranchAndBound(square) bb.solve() pi, pi0 = bb.find_strong_disjunctive_cut(0) # check cut is what we expect, i.e. x1 <= 1 assert_allclose(pi / pi0, np.array([1, 0]), atol=.01) self.assertTrue((pi - .01 < 0).all()) self.assertTrue(pi0 - .01 < 0) # check we get same bound A = np.append(bb.root_node.lp.coefMatrix.toarray(), [pi], axis=0) b = np.append(bb.root_node.lp.constraintsLower, pi0, axis=0) warm_model = MILPInstance(A=A, b=b, c=bb.root_node.lp.objective, l=bb.root_node.lp.variablesLower.copy(), u=bb.root_node.lp.variablesUpper.copy(), sense=['Min', '>='], integerIndices=bb.root_node._integer_indices, numVars=bb.root_node.lp.nVariables) warm_bb = BranchAndBound(warm_model) warm_bb.solve() # self.assertTrue(bb.global_lower_bound == warm_bb.root_node.objective_value) # try another problem bb = BranchAndBound(small_branch, node_limit=10) bb.solve() pi, pi0 = bb.find_strong_disjunctive_cut(0) # check cut is what we expect, i.e. x3 <= 1 assert_allclose(pi / pi0, np.array([0, 0, 1]), atol=.01) self.assertTrue((pi - .01 < 0).all()) self.assertTrue(pi0 - .01 < 0) # check we get same bound A = np.append(bb.root_node.lp.coefMatrix.toarray(), [pi], axis=0) b = np.append(bb.root_node.lp.constraintsLower, pi0, axis=0) warm_model = MILPInstance(A=A, b=b, c=bb.root_node.lp.objective, l=bb.root_node.lp.variablesLower.copy(), u=bb.root_node.lp.variablesUpper.copy(), sense=['Min', '>='], integerIndices=bb.root_node._integer_indices, numVars=bb.root_node.lp.nVariables) warm_bb = BranchAndBound(warm_model) warm_bb.solve()
def generate_random_MILPInstance(numVars=40, numCons=20, density=0.2, maxObjCoeff=10, maxConsCoeff=10, tightness=2, rand_seed=2): cs, vs, objective, A, b = GenerateRandomMIP(numVars=numVars, numCons=numCons, density=density, maxObjCoeff=maxObjCoeff, maxConsCoeff=maxConsCoeff, tightness=tightness, rand_seed=rand_seed) A = np.asmatrix(pd.DataFrame.from_dict(A).to_numpy()) objective = CyLPArray(list(objective.values())) b = CyLPArray(b) l = CyLPArray([0] * len(vs)) u = CyLPArray([maxObjCoeff] * len(vs)) return MILPInstance(A=A, b=b, c=objective, l=l, u=u, sense=['Max', '<='], integerIndices=list(range(len(vs))), numVars=len(vs))
def _optimize_cut(self: G, pi: np.ndarray, cut_optimization_node_limit: int = 10, **kwargs: Any) -> float: """ Given the valid inequality pi >= pi0, try to find a smaller RHS such that pi >= smaller RHS is still a valid inequality :param pi: coefficients of the vector to optimize :param cut_optimization_node_limit: maximimum number of nodes to evaluate before terminating :param kwargs: catch all for unused passed kwargs :return: the objective value of the best milp feasible solution found """ A = self.lp.coefMatrix.toarray() assert A.shape[1] == pi.shape[ 0], 'number of columns of A and length of c should match' # make new model where we minimize the cut model = MILPInstance(A=A, b=self.lp.constraintsLower.copy(), c=pi, sense=['Min', '>='], integerIndices=self._integer_indices, numVars=pi.shape[0]) # find a tighter bound with branch and bound bb = BranchAndBound(model=model, Node=BaseNode, node_limit=cut_optimization_node_limit, pseudo_costs={}) bb.solve() return bb.objective_value if bb.status == 'optimal' else bb.global_lower_bound
def test_sense(self): node = BaseNode(small_branch.lp, small_branch.integerIndices, 0) self.assertTrue(node._sense == '<=') milp = MILPInstance(A=small_branch.A, b=small_branch.b, c=small_branch.c, sense=['Min', '>='], numVars=3, integerIndices=small_branch.integerIndices) node = BaseNode(milp.lp, milp.integerIndices, 0) self.assertTrue(node._sense == '>=')
def _convert_constraints_to_greq(model: MILPInstance) -> MILPInstance: """ If constraints are of the form A <= b, convert them to A >= b :return: Updated MILPInstance with constraints turned around """ if model.sense == '<=': # all problems converted to minimization via lp.objective in MILPInstance init if isinstance(model.A, csc_matrixPlus): model.A = model.A.toarray() return MILPInstance(A=-model.A, b=-model.b, c=model.lp.objective, l=model.l, u=model.u, integerIndices=model.integerIndices, sense=['Min', '>='], numVars=len(model.c)) else: return model
def base_test_models(self): self.assertTrue(gu, 'gurobipy needed for this test') fldr = os.path.join( os.path.dirname( os.path.abspath(inspect.getfile(generate_random_variety))), 'example_models') for i, file in enumerate(os.listdir(fldr)): print(f'running test {i + 1}') pth = os.path.join(fldr, file) model = MILPInstance(file_name=pth) bb = BranchAndBound(model, self.Node) bb.solve() gu_mdl = gu.read(pth) gu_mdl.optimize() self.assertTrue( isclose(bb.objective_value, gu_mdl.objVal, abs_tol=.01), f'different for {file}')
def setUp(self) -> None: # reset models each test so lps dont keep added constraints for name, m in { 'cut1_std': cut1, 'cut2_std': cut2, 'infeasible_std': infeasible, 'unbounded': unbounded }.items(): new_m = MILPInstance(A=m.A, b=m.b, c=m.lp.objective, l=m.l, sense=['Min', m.sense], integerIndices=m.integerIndices, numVars=len(m.lp.objective)) new_m = Utils._convert_constraints_to_greq(new_m) setattr(self, name, new_m)
def generate_random_value_functions(num_probs=40, num_evals=40): for prob_num in range(num_probs): num_vars = 4 num_constrs = 2 density = np.random.uniform(.2, .8) max_obj_coef = np.random.randint(10, 100) max_const_coef = np.random.randint(10, 100) tightness = 15 # get a random A and objective cs, vs, objective, A, b = GenerateRandomMIP( numVars=num_vars, numCons=num_constrs, density=density, maxObjCoeff=max_obj_coef, maxConsCoeff=max_const_coef, tightness=15) A = np.asmatrix(pd.DataFrame.from_dict(A).to_numpy()) objective = CyLPArray(list(objective.values())) l = CyLPArray([0] * len(vs)) u = CyLPArray([max_obj_coef] * len(vs)) # make a new directory to save all these instances fldr = f'example_value_functions/instance_{prob_num}' os.mkdir(fldr) for eval_num in range(num_evals): # make a random b for each evaluation of this instance b = CyLPArray([ np.random.randint( int(num_vars * density * max_const_coef / tightness), int(num_vars * density * max_const_coef / 1.5)) for i in range(num_constrs) ]) instance = MILPInstance(A=A, b=b, c=objective, l=l, u=u, sense=['Max', '<='], integerIndices=list(range(len(vs))), numVars=len(vs)) for i in instance.integerIndices: instance.lp.setInteger(i) file = f'evaluation_{eval_num}' instance.lp.writeMps(f'{os.path.join(fldr, file)}.mps')
def base_test_models(self, standardize_model=False): self.assertTrue(gu, 'gurobipy needed for this test') fldr = os.path.join( os.path.dirname(os.path.abspath(inspect.getfile(generate_random_variety))), 'example_models' ) for i, file in enumerate(os.listdir(fldr)): print(f'running test {i + 1}') pth = os.path.join(fldr, file) model = MILPInstance(file_name=pth) bb = BranchAndBound(model, self.Node, pseudo_costs={}) bb.solve() gu_mdl = gu.read(pth) gu_mdl.setParam(gu.GRB.Param.LogToConsole, 0) gu_mdl.optimize() if not isclose(bb.objective_value, gu_mdl.objVal, abs_tol=.01): print(f'different for {file}') print(f'mine: {bb.objective_value}') print(f'gurobi: {gu_mdl.objVal}') self.assertTrue(isclose(bb.objective_value, gu_mdl.objVal, abs_tol=.01), f'different for {file}')
def test_find_strong_disjunctive_cut_many_times(self): fldr = os.path.join( os.path.dirname( os.path.abspath(inspect.getfile(generate_random_variety))), 'example_models') for i, file in enumerate(os.listdir(fldr)): if i >= 10: break print(f'running test {i + 1}') pth = os.path.join(fldr, file) model = MILPInstance(file_name=pth) bb = BranchAndBound(model) bb.solve() pi, pi0 = bb.find_strong_disjunctive_cut(0) # ensure we cut off the root solution self.assertTrue(sum(pi * bb.root_node.solution) <= pi0) # ensure we don't cut off disjunctive mins for n in bb.tree.get_leaves(0): if n.lp_feasible: self.assertTrue(sum(pi * n.solution) >= pi0 - .01)
def test_dual_bound_many_times(self): pattern = re.compile('evaluation_(\d+).mps') fldr_pth = os.path.join( os.path.dirname(os.path.abspath(inspect.getfile(example_models))), 'example_value_functions') for count, sub_fldr in enumerate(os.listdir(fldr_pth)): print(f'dual bound {count}') sub_fldr_pth = os.path.join(fldr_pth, sub_fldr) evals = {} for file in os.listdir(sub_fldr_pth): eval_num = int(pattern.search(file).group(1)) instance = MILPInstance( file_name=os.path.join(sub_fldr_pth, file)) bb = BranchAndBound(instance, PseudoCostBranchNode, pseudo_costs={}) bb.solve() evals[eval_num] = bb instance_0 = evals[0] for bb in evals.values(): # all problems were given as <=, so their constraints were flipped by default self.assertTrue( instance_0.dual_bound(CyLPArray(-bb.model.b)) <= bb.objective_value + .01, 'dual_bound should be less')
def main(cut_offs, in_fldr, out_file='warm_start_comparison.csv'): assert ((np.array([0] + cut_offs)) < (np.array(cut_offs + [float('inf')]))).all(), \ 'please put cut off sizes in increasing order' Path(out_file).unlink(missing_ok=True) # delete output file if it exists for i, file in enumerate(os.listdir(in_fldr)): print(f'running test {i + 1}') warm_bb = {} data = {} pth = os.path.join(in_fldr, file) model = MILPInstance(file_name=pth) # cold started branch and bound cold_bb = BranchAndBound(model, PseudoCostBranchNode, pseudo_costs={}) for c in cut_offs: cold_bb.node_limit = c cold_bb.solve() start = time.process_time() pi, pi0 = cold_bb.find_strong_disjunctive_cut(0) cglp_time = time.process_time() - start # warm start branch and bound with disjunctive cut after <c> nodes A = np.append(cold_bb.root_node.lp.coefMatrix.toarray(), [pi], axis=0) b = np.append(cold_bb.root_node.lp.constraintsLower, pi0, axis=0) warm_model = MILPInstance( A=A, b=b, c=cold_bb.root_node.lp.objective, l=cold_bb.root_node.lp.variablesLower.copy(), u=cold_bb.root_node.lp.variablesUpper.copy(), sense=['Min', '>='], integerIndices=cold_bb.root_node._integer_indices, numVars=cold_bb.root_node.lp.nVariables ) # get data to compare starts and progress after <c> node evaluations # for both warm and cold starts warm_bb[c] = BranchAndBound(warm_model, PseudoCostBranchNode, node_limit=c, pseudo_costs={}) warm_bb[c].solve() data[c] = { 'cold initial lower bound': cold_bb.root_node.objective_value, 'warm initial lower bound': warm_bb[c].root_node.objective_value, 'cold cut off lower bound': cold_bb.global_lower_bound, 'warm cut off lower bound': warm_bb[c].global_lower_bound, 'cut off time': cold_bb.solve_time, 'cglp time': cglp_time } # get data on warm start termination warm_bb[c].node_limit = float('inf') warm_bb[c].solve() data[c]['warm evaluated nodes'] = warm_bb[c].evaluated_nodes data[c]['warm solve time'] = warm_bb[c].solve_time data[c]['total restart solve time'] = data[c]['cut off time'] + \ data[c]['cglp time'] + warm_bb[c].solve_time data[c]['total restart evaluated nodes'] = cold_bb.evaluated_nodes + \ warm_bb[c].evaluated_nodes data[c]['warm initial gap'] = \ abs(warm_bb[c].objective_value - data[c]['warm initial lower bound']) / \ abs(warm_bb[c].objective_value) data[c]['warm cut off gap'] = \ abs(warm_bb[c].objective_value - data[c]['warm cut off lower bound']) / \ abs(warm_bb[c].objective_value) data[c]['warm objective value'] = warm_bb[c].objective_value # get data on cold start termination cold_bb.node_limit = float('inf') cold_bb.solve() for c in cut_offs: assert cold_bb.global_lower_bound <= warm_bb[c].global_upper_bound + .01 and \ cold_bb.global_upper_bound + .01 >= warm_bb[c].global_lower_bound, \ 'gaps should overlap' data[c]['cold initial gap'] = \ abs(cold_bb.objective_value - data[c]['cold initial lower bound']) / \ abs(cold_bb.objective_value) data[c]['cold cut off gap'] = \ abs(cold_bb.objective_value - data[c]['cold cut off lower bound']) / \ abs(cold_bb.objective_value) data[c]['cold evaluated nodes'] = cold_bb.evaluated_nodes data[c]['cold solve time'] = cold_bb.solve_time data[c]['cold objective value'] = cold_bb.objective_value data[c]['initial gap improvement ratio'] = \ (data[c]['cold initial gap'] - data[c]['warm initial gap']) / \ data[c]['cold initial gap'] data[c]['cut off gap improvement ratio'] = \ (data[c]['cold cut off gap'] - data[c]['warm cut off gap']) / \ data[c]['cold cut off gap'] data[c]['warm evaluated nodes ratio'] = \ (data[c]['cold evaluated nodes'] - data[c]['warm evaluated nodes']) / \ data[c]['cold evaluated nodes'] data[c]['warm solve time ratio'] = \ (data[c]['cold solve time'] - data[c]['warm solve time']) / \ data[c]['cold solve time'] data[c]['total restart evaluated nodes ratio'] = \ (data[c]['cold evaluated nodes'] - data[c]['total restart evaluated nodes']) / \ data[c]['cold evaluated nodes'] data[c]['total restart solve time ratio'] = \ (data[c]['cold solve time'] - data[c]['total restart solve time']) / \ data[c]['cold solve time'] # append this test to our file df = pd.DataFrame.from_dict(data, orient='index') df.index.names = ['cut off'] df.reset_index(inplace=True) df['test number'] = [i]*len(cut_offs) # rearrange columns cols = [ 'test number', 'cut off', 'initial gap improvement ratio', 'cut off gap improvement ratio', 'warm evaluated nodes ratio', 'total restart evaluated nodes ratio', 'warm solve time ratio', 'total restart solve time ratio', 'cold objective value', 'cold initial lower bound', 'cold initial gap', 'cold cut off lower bound', 'cold cut off gap', 'warm objective value', 'warm initial lower bound', 'warm initial gap', 'warm cut off lower bound', 'warm cut off gap', 'cold evaluated nodes', 'warm evaluated nodes', 'total restart evaluated nodes', 'cold solve time', 'cut off time', 'cglp time', 'warm solve time', 'total restart solve time' ] df = df[cols] with open(out_file, 'a') as f: df.to_csv(f, mode='a', header=f.tell() == 0, index=False)
numVars = 2 points = [[2.5, 4.5], [6.5, 0.5], [0.5, 1], [7, 5.7], [7.7, 5], [2, 0.25]] #points = [[0, 0], [2.5, 4.5], [7.75, 5.75], [6.5, 0.5]] #points = [[0, 0], [2.5, 4.5], [7.5, 5.5], [6.5, 0.5]] rays = [] c = [0, 1] sense = ('Max', '<=') integerIndices = [0, 1] if __name__ == '__main__': try: from coinor.cuppy.cuttingPlanes import solve, gomoryCut from coinor.cuppy.milpInstance import MILPInstance except ImportError: from src.cuppy.cuttingPlanes import solve, gomoryCut from src.cuppy.milpInstance import MILPInstance m = MILPInstance(c=c, points=points, rays=rays, sense=sense, integerIndices=integerIndices, numVars=numVars) solve(m, whichCuts=[(gomoryCut, {})], display=True, debug_print=True)
b = [ #28, 27, # 1, 0, 0 ] c = [2, 5] obj_val = 2 sense = ('Max', '<=') integerIndices = [0, 1] if __name__ == '__main__': try: from coinor.cuppy.cuttingPlanes import solve, gomoryCut from coinor.cuppy.milpInstance import MILPInstance except ImportError: from src.cuppy.cuttingPlanes import solve, gomoryCut from src.cuppy.milpInstance import MILPInstance m = MILPInstance(A=A, b=b, c=c, sense=sense, integerIndices=integerIndices, numVars=numVars) solve(m, whichCuts=[(gomoryCut, {})], display=True, debug_print=True)
f'_density_{densities[density]}' f'_max_obj_coeff_{max_obj_coeffs[max_obj_coeff]}' f'_max_cons_coeff_{max_cons_coeffs[max_cons_coeff]}' f'_tightness_{tightnesses[tightness]}.mps') # ----------------- a model with MIP feasible LP relaxation ----------------- A = np.matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) b = CyLPArray([1, 1, 1]) c = CyLPArray([1, 1, 0]) l = CyLPArray([0, 0, 0]) no_branch = MILPInstance(A=A, b=b, c=c, l=l, sense=['Max', '<='], integerIndices=[0, 1], numVars=3) # ----------- a small model that needs to branch to solve the MIP ----------- A = np.matrix([[1, 0, 1], [0, 1, 0]]) b = CyLPArray([1.5, 1.25]) c = CyLPArray([1, 1, 1]) l = CyLPArray([0, 0, 0]) small_branch = MILPInstance(A=A, b=b, c=c, l=l, sense=['Max', '<='],
for i in instance.integerIndices: instance.lp.setInteger(i) file = f'evaluation_{eval_num}' instance.lp.writeMps(f'{os.path.join(fldr, file)}.mps') # ----------------- a model with MIP feasible LP relaxation ----------------- A = np.matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) b = CyLPArray([1, 1, 1]) c = CyLPArray([1, 1, 0]) l = CyLPArray([0, 0, 0]) no_branch = MILPInstance(A=A, b=b, c=c, l=l, sense=['Max', '<='], integerIndices=[0, 1], numVars=3) # ----------- a small model that needs to branch to solve the MIP ----------- A = np.matrix([[1, 0, 1], [0, 1, 0]]) b = CyLPArray([1.5, 1.25]) c = CyLPArray([1, 1, 1]) l = CyLPArray([0, 0, 0]) u = CyLPArray([10, 10, 10]) small_branch = MILPInstance(A=A, b=b, c=c, l=l,