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_solve_past_node_limit(self): bb = BranchAndBound(unbounded, node_limit=10) # check we quit even if node_queue nonempty with patch.object(bb, '_evaluate_node') as en: bb.evaluated_nodes = 10 bb.solve() self.assertFalse(en.called, 'were past the node limit')
def test_solve_infeasible(self): # check and make sure we're good with both nodes for Node in [BaseNode, PCBDFSNode]: bb = BranchAndBound(infeasible, Node=Node) bb.solve() self.assertTrue(bb.status == 'infeasible') self.assertFalse(bb.solution) self.assertTrue(bb.objective_value == float('inf'))
def test_solve_optimal(self): # check and make sure we're good with both nodes for Node in [BaseNode, PCBDFSNode]: bb = BranchAndBound(small_branch, Node=Node) bb.solve() self.assertTrue(bb.status == 'optimal') self.assertTrue(all(s.is_integer for s in bb.solution)) self.assertTrue(bb.objective_value == -2)
def test_evaluate_node_unbounded(self): bb = BranchAndBound(unbounded) bb._evaluate_node(bb.root_node) # check attributes self.assertTrue(bb._unbounded) self.assertTrue(bb.evaluated_nodes == 1, 'only one node should be evaluated')
def test_get_leaves(self): bb = BranchAndBound(small_branch) bb.solve() leaves = bb.tree.get_leaves(0) for node_id, node in bb.tree.nodes.items(): if node_id in [n.idx for n in leaves]: self.assertFalse(bb.tree.get_children(node_id)) else: self.assertTrue(len(bb.tree.get_children(node_id)) == 2)
def test_solve_stopped_on_iterations(self): # check and make sure we're good with both nodes for Node in [BaseNode, PCBDFSNode]: bb = BranchAndBound(small_branch, Node=Node, node_limit=1, pseudo_costs={}) bb.solve() self.assertTrue(bb.status == 'stopped on iterations') self.assertTrue(bb.solve_time)
def test_current_gap(self): bb = BranchAndBound(small_branch, node_limit=1) bb.solve() self.assertTrue(bb.current_gap is None) bb.node_limit = 10 bb.solve() self.assertTrue(bb.current_gap == .125) bb.node_limit = float('inf') bb.solve() self.assertTrue(bb.current_gap == 0) print()
def test_get_node_instances_fails_asserts(self): bb = BranchAndBound(small_branch) bb.solve() self.assertRaisesRegex(AssertionError, 'must be an integer or iterable', bb.tree.get_node_instances, '1') self.assertRaisesRegex(AssertionError, 'node_ids are not in the tree', bb.tree.get_node_instances, [20]) del bb.tree.nodes[0].attr['node'] self.assertRaisesRegex(AssertionError, 'must have an attribute for a node instance', bb.tree.get_node_instances, [0])
def test_process_branch_rtn(self): bb = BranchAndBound(small_branch) node = BaseNode(small_branch.lp, small_branch.integerIndices, idx=0) node.bound() rtn = node.branch(next_node_idx=1) left_node = rtn['left'] right_node = rtn['right'] bb._process_branch_rtn(node.idx, rtn) # check attributes self.assertTrue(isinstance(bb._node_queue.get(), BaseNode)) self.assertTrue(isinstance(bb._node_queue.get(), BaseNode)) self.assertTrue(bb._node_queue.empty()) children = bb.tree.get_children(node.idx) self.assertTrue(len(children) == 2, 'there should be two kids created') for child in children: self.assertFalse(bb.tree.get_children(child), 'children shouldnt have kids') self.assertTrue(bb.tree.get_node(1).attr['node'] is left_node) self.assertTrue(bb.tree.get_node(2).attr['node'] is right_node) # check function calls bb = BranchAndBound(small_branch) node = BaseNode(small_branch.lp, small_branch.integerIndices, 0) node.bound() rtn = node.branch() with patch.object(bb, '_process_rtn') as pr, \ patch.object(bb.tree, 'add_left_child') as alc, \ patch.object(bb.tree, 'add_right_child') as arc: bb._process_branch_rtn(0, rtn) self.assertTrue(pr.call_count == 1, 'should call process rtn') self.assertTrue(alc.call_count == 1, 'should call add left child') self.assertTrue(arc.call_count == 1, 'should call add right child')
def test_evaluate_node_properly_prunes(self): bb = BranchAndBound(no_branch) bb._global_upper_bound = -2 called_node = BaseNode(bb.model.lp, bb.model.integerIndices, -4) pruned_node = BaseNode(bb.model.lp, bb.model.integerIndices, 0) with patch.object(called_node, 'bound') as cnb, \ patch.object(pruned_node, 'bound') as pnb: cnb.return_value = {} pnb.return_value = {} bb._node_queue.put(called_node) bb._node_queue.put(pruned_node) bb._evaluate_node(bb._node_queue.get()) bb._evaluate_node(bb._node_queue.get()) self.assertTrue(cnb.call_count == 1, 'first node should run') self.assertFalse(pnb.call_count, 'second node should get pruned') self.assertTrue(bb._node_queue.empty())
def test_solve_unbounded(self): # check and make sure we're good with both nodes for Node in [BaseNode, PCBDFSNode]: bb = BranchAndBound(unbounded, Node=Node) bb.solve() self.assertTrue(bb.status == 'unbounded') # check we quit even if node_queue nonempty with patch.object(bb, '_evaluate_node') as en: bb = BranchAndBound(unbounded) bb._unbounded = True bb.solve() self.assertFalse(en.called)
def test_process_branch_rtn_fails_asserts(self): bb = BranchAndBound(small_branch) self.assertRaisesRegex(AssertionError, 'rtn must be a dictionary', bb._process_rtn, 'fish') rtn = {'up': 5, 'down': 5} self.assertRaisesRegex(AssertionError, 'value must be type', bb._process_branch_rtn, rtn) del rtn['up'] self.assertRaisesRegex(AssertionError, 'must be in the returned', bb._process_branch_rtn, rtn)
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_evaluate_node_fractional(self): bb = BranchAndBound(small_branch, Node=PCBDFSNode, pseudo_costs={}, strong_branch_iters=5) bb._evaluate_node(bb.root_node) # check attributes self.assertFalse(bb._best_solution, 'best solution should not change') self.assertTrue(bb.global_upper_bound == float('inf'), 'shouldnt change') self.assertTrue(bb.global_lower_bound > -float('inf'), 'should change') self.assertTrue(bb._node_queue.qsize() == 2, 'should branch and add two nodes') self.assertTrue(bb._kwargs['pseudo_costs'], 'something should be set') self.assertTrue(bb._kwargs['strong_branch_iters'], 'something should be set') self.assertTrue(bb.evaluated_nodes == 1, 'only one node should be evaluated') # check function calls - recycle object since it has attrs already set with patch.object(bb, '_process_rtn') as pr, \ patch.object(bb, '_process_branch_rtn') as pbr, \ patch.object(bb.root_node, 'bound') as bd, \ patch.object(bb.root_node, 'branch') as bh: bb._evaluate_node(bb.root_node) self.assertTrue(pr.call_count == 1) # direct calls self.assertTrue(pbr.call_count == 1) self.assertTrue(0 == pbr.call_args.args[0], 'root node id should be first call arg') self.assertTrue(bd.call_count == 1) self.assertTrue(bh.call_count == 1)
def test_evaluate_node_infeasible(self): bb = BranchAndBound(infeasible) bb._evaluate_node(bb.root_node) # check attributes self.assertTrue(bb._node_queue.empty(), 'inf model should create no nodes') self.assertFalse(bb._best_solution, 'best solution should not change') self.assertTrue(bb.global_upper_bound == float('inf'), 'shouldnt change') self.assertTrue(bb.global_lower_bound == float('inf'), 'shouldnt change') self.assertTrue(bb.evaluated_nodes == 1, 'only one node should be evaluated') # check function calls - recycle object since it has attrs already set with patch.object(bb, '_process_rtn') as pr, \ patch.object(bb, '_process_branch_rtn') as pbr, \ patch.object(bb.root_node, 'bound') as bd, \ patch.object(bb.root_node, 'branch') as bh: bb._evaluate_node(bb.root_node) self.assertTrue(pr.call_count == 1) self.assertTrue(pbr.call_count == 0) self.assertTrue(bd.call_count == 1) self.assertTrue(bh.call_count == 0)
def test_init(self): bb = BranchAndBound(small_branch) self.assertTrue(bb._global_upper_bound == float('inf')) self.assertTrue(bb._node_queue.empty()) self.assertTrue(inspect.isclass(bb._Node)) self.assertTrue(bb._root_node) self.assertTrue(bb.model) self.assertFalse(bb._unbounded) self.assertFalse(bb._best_solution) self.assertFalse(bb.solution) self.assertTrue(bb.status == 'unsolved') self.assertFalse(bb.objective_value) self.assertFalse(bb._pseudo_costs, 'should exist but be empty') self.assertTrue(bb._strong_branch_iters == 5)
def test_init_fails_asserts(self): lp = CyClpSimplex() bb = BranchAndBound(small_branch) queue = PriorityQueue() # model asserts self.assertRaisesRegex(AssertionError, 'model must be cuppy MILPInstance', BranchAndBound, lp) # Node asserts self.assertRaisesRegex(AssertionError, 'Node must be a class', BranchAndBound, small_branch, 'Node') for attribute in bb._node_attributes: class BadNode(BaseNode): def __init__(self, **kwargs): super().__init__(**kwargs) delattr(self, attribute) self.assertRaisesRegex(AssertionError, f'Node needs a {attribute}', BranchAndBound, small_branch, BadNode) for func in bb._node_funcs: class BadNode(BaseNode): def __init__(self, **kwargs): super().__init__(**kwargs) self.__dict__[func] = 5 self.assertRaisesRegex(AssertionError, f'Node needs a {func}', BranchAndBound, small_branch, BadNode) # node_queue asserts for func in reversed(bb._queue_funcs): queue.__dict__[func] = 5 self.assertRaisesRegex(AssertionError, f'node_queue needs a {func} function', BranchAndBound, small_branch, BaseNode, queue) # strong branch iters asserts self.assertRaisesRegex( AssertionError, 'strong branching iterations must be positive integer', BranchAndBound, small_branch, strong_branch_iters=-1)
def test_process_branch_rtn(self): bb = BranchAndBound(small_branch) node = BaseNode(small_branch.lp, small_branch.integerIndices) node.bound() rtn = node.branch() bb._process_branch_rtn(rtn) # check attributes self.assertTrue(isinstance(bb._node_queue.get(), BaseNode)) self.assertTrue(isinstance(bb._node_queue.get(), BaseNode)) self.assertTrue(bb._node_queue.empty()) # check function calls bb = BranchAndBound(small_branch) node = BaseNode(small_branch.lp, small_branch.integerIndices) node.bound() rtn = node.branch() with patch.object(bb, '_process_rtn') as pr: bb._process_branch_rtn(rtn) self.assertTrue(pr.call_count == 1, 'should call rtn')
def test_dual_bound_fails_asserts(self): bb = BranchAndBound(small_branch) self.assertRaisesRegex(AssertionError, 'must solve this instance before', bb.dual_bound, CyLPArray([2.5, 4.5])) bb.solve() self.assertRaisesRegex(AssertionError, 'only works with CyLP arrays', bb.dual_bound, np.array([2.5, 4.5])) self.assertRaisesRegex(AssertionError, 'shape of the RHS being added should match', bb.dual_bound, CyLPArray([4.5])) bb = BranchAndBound(infeasible2) bb.root_node.lp += np.matrix( [[0, -1, -1]]) * bb.root_node.lp.getVarByName('x') >= CyLPArray( [-2.5]) bb.solve() self.assertRaisesRegex( AssertionError, 'feature expects the root node to have a single constraint object', bb.dual_bound, CyLPArray([2.5, 4.5]))
def test_evaluate_node_integer(self): bb = BranchAndBound(no_branch) bb._evaluate_node(bb._root_node) # check attributes self.assertTrue(all(bb._best_solution == [1, 1, 0])) self.assertTrue(bb._global_upper_bound == -2) self.assertTrue(bb._node_queue.empty(), 'immediately optimal model should create no nodes') # check function calls - recycle object since it has attrs already set with patch.object(bb, '_process_rtn') as pr, \ patch.object(bb, '_process_branch_rtn') as pbr, \ patch.object(bb._root_node, 'bound') as bd, \ patch.object(bb._root_node, 'branch') as bh: bb._evaluate_node(bb._root_node) self.assertTrue(pr.call_count == 1) self.assertTrue(pbr.call_count == 0) self.assertTrue(bd.call_count == 1) self.assertTrue(bh.call_count == 0)