コード例 #1
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')
コード例 #2
0
    def test_branch(self):
        node = BaseNode(small_branch.lp, small_branch.integerIndices)
        node.bound()

        # check function calls
        mock_pth = 'simple_mip_solver.nodes.base_node.BaseNode._most_fractional_index'
        with patch(mock_pth, new_callable=PropertyMock) as mfi, \
                patch.object(node, '_base_branch') as bb:
            node.branch(junk='stuff')  # should work with extra args
            self.assertTrue(mfi.call_count == 1, 'should call most frac idx')
            self.assertTrue(bb.call_count == 1, 'should call base branch')

        # check returns
        nodes = node.branch()
        self.assertTrue(all(isinstance(n, BaseNode) for n in nodes.values()))
コード例 #3
0
    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')
コード例 #4
0
    def test_branch(self):
        node = BaseNode(small_branch.lp, small_branch.integerIndices, 0)
        node.bound()

        # check function calls
        mock_pth = 'simple_mip_solver.nodes.base_node.BaseNode._most_fractional_index'
        with patch(mock_pth, new_callable=PropertyMock) as mfi, \
                patch.object(node, '_base_branch') as bb:
            bb_rtn = {'left': 'mock', 'right': 'another_mock', 'next_node_idx': 3}
            bb.return_value = bb_rtn
            branch_rtn = node.branch(junk='stuff')  # should work with extra args
            self.assertTrue(mfi.call_count == 1, 'should call most frac idx')
            self.assertTrue(bb.call_count == 1, 'should call base branch')
            self.assertTrue(branch_rtn == bb_rtn)
コード例 #5
0
class TestBranchAndBound(unittest.TestCase):
    def setUp(self) -> None:
        self.bb = BranchAndBound(small_branch)
        self.unbounded_root = BaseNode(small_branch.lp,
                                       small_branch.integerIndices)
        self.bound_root = BaseNode(small_branch.lp,
                                   small_branch.integerIndices)
        self.bound_root.bound()
        self.root_branch_rtn = self.bound_root.branch()

    def test_init(self):
        bb = BranchAndBound(small_branch)
        self.assertTrue(isinstance(bb, Utils))
        self.assertTrue(bb.global_upper_bound == float('inf'))
        self.assertTrue(bb._node_queue.empty())
        self.assertFalse(bb._unbounded)
        self.assertFalse(bb._best_solution)
        self.assertFalse(bb.solution)
        self.assertTrue(bb.status == 'unsolved')
        self.assertFalse(bb.objective_value)
        self.assertTrue(isinstance(bb.tree, BranchAndBoundTree))
        self.assertTrue(list(bb.tree.nodes.keys()) == [0])
        self.assertTrue(bb.tree.nodes[0].attr['node'] is bb.root_node)
        self.assertFalse(bb.solve_time, 'solve time should exist and be 0')
        self.assertTrue(bb.mip_gap, 'mip gap should be an attribute')

    def test_init_fails_asserts(self):
        bb = BranchAndBound(small_branch)
        queue = PriorityQueue()

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

        # node limit asserts
        self.assertRaisesRegex(AssertionError,
                               f'node limit',
                               BranchAndBound,
                               model=small_branch,
                               node_limit=-5)

        # mip gap asserts
        self.assertRaisesRegex(AssertionError,
                               f'mip_gap',
                               BranchAndBound,
                               model=small_branch,
                               mip_gap=-5)

        # kwargs asserts
        self.assertRaisesRegex(AssertionError,
                               f'keys "right"',
                               BranchAndBound,
                               model=small_branch,
                               right=-5)

    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_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_solve_optimal(self):
        # check and make sure we're good with both nodes
        for Node in [BaseNode, PCBDFSNode]:
            bb = BranchAndBound(small_branch, Node=Node, pseudo_costs={})
            bb.solve()
            self.assertTrue(bb.status == 'optimal')
            self.assertTrue(all(s.is_integer for s in bb.solution))
            self.assertTrue(bb.objective_value == -2)
            self.assertTrue(bb.solve_time)

    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, pseudo_costs={})
            bb.solve()
            self.assertTrue(bb.status == 'infeasible')
            self.assertFalse(bb.solution)
            self.assertTrue(bb.objective_value == float('inf'))
            self.assertTrue(bb.solve_time)

    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, pseudo_costs={})
            bb.solve()
            self.assertTrue(bb.status == 'unbounded')
            self.assertTrue(bb.solve_time)

        # 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_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_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_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_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.global_lower_bound == -2,
                        'should match upper bound when optimal')
        self.assertTrue(bb._node_queue.empty(),
                        'immediately optimal model should create no nodes')
        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_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_evaluate_node_properly_prunes(self):
        bb = BranchAndBound(no_branch)
        bb.global_upper_bound = -2
        called_node = BaseNode(bb.model.lp,
                               bb.model.integerIndices,
                               lower_bound=-4)
        pruned_node = BaseNode(bb.model.lp,
                               bb.model.integerIndices,
                               lower_bound=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())
            self.assertTrue(
                bb.evaluated_nodes == 1,
                'only one node should be evaluated since other pruned')

    def test_process_branch_rtn_fails_asserts(self):
        self.assertRaisesRegex(AssertionError, 'rtn must be a dictionary',
                               self.bb._process_branch_rtn, 0, 'fish')
        rtn = {'right': 5, 'left': 5}
        self.assertRaisesRegex(AssertionError, 'must be int',
                               self.bb._process_branch_rtn, '0', rtn)
        self.assertRaisesRegex(AssertionError, 'must already exist in tree',
                               self.bb._process_branch_rtn, 1, rtn)
        self.assertRaisesRegex(AssertionError, 'value must be type',
                               self.bb._process_branch_rtn, 0, rtn)
        del rtn['left']
        self.assertRaisesRegex(AssertionError, 'must be in the returned',
                               self.bb._process_branch_rtn, 0, rtn)
        self.root_branch_rtn['right'].idx = 0

        # test node index against rest of tree
        rtn = self.bound_root.branch(next_node_idx=1)
        rtn['left'].idx = 0
        self.assertRaisesRegex(AssertionError, 'give unique node ID',
                               self.bb._process_branch_rtn, 0, rtn)

    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_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_dual_bound(self):

        # Ensure that BranchAndBound.dual_bound generates the dual function
        # that we saw in ISE 418 HW 3 problem 1
        bb = BranchAndBound(h3p1)
        bb.solve()
        bound = bb.dual_bound(CyLPArray([3.5, -3.5]))
        self.assertTrue(bb.objective_value == bound,
                        'dual should be strong at original rhs')

        prob = {
            0: h3p1_0,
            1: h3p1_1,
            2: h3p1_2,
            3: h3p1_3,
            4: h3p1_4,
            5: h3p1_5
        }
        sol_new = {0: 0, 1: 1, 2: 1, 3: 2, 4: 2, 5: 3}
        sol_bound = {0: 0, 1: .5, 2: 1, 3: 2, 4: 2, 5: 2.5}
        for beta in range(6):
            new_bb = BranchAndBound(prob[beta])
            new_bb.solve()
            bound = bb.dual_bound(CyLPArray(np.array([beta, -beta])))
            self.assertTrue(
                isclose(sol_new[beta], new_bb.objective_value, abs_tol=.01),
                'new branch and bound objective should match expected')
            self.assertTrue(isclose(sol_bound[beta], bound),
                            'new dual bound value should match expected')
            self.assertTrue(
                bound <= new_bb.objective_value + .01,
                'dual bound value should be at most the value function for this rhs'
            )

        bb = BranchAndBound(small_branch)
        bb.solve()
        bound = bb.dual_bound(CyLPArray([2.5, 4.5]))

        # just make sure the dual bound works here too
        self.assertTrue(
            bound <= -5.99,
            'dual bound value should be at most the value function for this rhs'
        )

        # check function calls
        bb = BranchAndBound(small_branch)
        bb.solve()
        bound_duals = [
            bb._bound_dual(n.lp)
            for n in bb.tree.get_node_instances([6, 12, 10, 8, 2])
        ]
        with patch.object(bb, '_bound_dual') as bd:
            bd.side_effect = bound_duals
            bound = bb.dual_bound(CyLPArray([3, 3]))
            self.assertTrue(bd.call_count == 5)

        bb = BranchAndBound(small_branch)
        bb.solve()
        bound = bb.dual_bound(CyLPArray([3, 3]))
        with patch.object(bb, '_bound_dual') as bd:
            bound = bb.dual_bound(CyLPArray([1, 1]))
            self.assertFalse(bd.called)

    @unittest.skipIf(skip_longs, "debugging")
    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 test_bound_dual(self):
        bb = BranchAndBound(infeasible2)
        bb.root_node.lp += np.matrix(
            [[0, -1, -1]]) * bb.root_node.lp.getVarByName('x') >= CyLPArray(
                [-2.5])
        bb.solve()
        terminal_nodes = bb.tree.get_leaves(0)
        infeasible_nodes = [
            n for n in terminal_nodes if n.lp_feasible is False
        ]
        n = infeasible_nodes[0]
        lp = bb._bound_dual(n.lp)

        # test that we get a CyClpSimplex object back
        self.assertTrue(isinstance(lp, CyClpSimplex),
                        'should return CyClpSimplex instance')

        # same variables plus extra 's'
        self.assertTrue(
            {v.name
             for v in lp.variables} == {'x', 's_0', 's_1'},
            'x should already exist and s_1 and s_2 should be added')
        old_x = n.lp.getVarByName('x')
        new_x, s_0, s_1 = lp.getVarByName('x'), lp.getVarByName(
            's_0'), lp.getVarByName('s_1')

        # same variable bounds, plus s >= 0
        self.assertTrue(
            all(new_x.lower == old_x.lower)
            and all(new_x.upper == old_x.upper),
            'x should have the same bounds')
        self.assertTrue(
            all(s_0.lower == [0, 0]) and all(s_0.upper > [1e300, 1e300]),
            's_0 >= 0')
        self.assertTrue(
            all(s_1.lower == [0]) and all(s_1.upper > 1e300), 's_1 >= 0')

        # same constraints, plus slack s
        self.assertTrue(lp.nConstraints == 3,
                        'should have same number of constraints')
        self.assertTrue(
            (lp.constraints[0].varCoefs[new_x] == np.array([[-1, -1, 0],
                                                            [0, 0,
                                                             -1]])).all(),
            'x coefs should stay same')
        self.assertTrue((lp.constraints[0].varCoefs[s_0] == np.matrix(
            np.identity(2))).all(), 's_0 should have coef of 2-D identity')
        self.assertTrue(
            all(lp.constraints[1].varCoefs[new_x] == np.array([0, -1, -1])),
            'x coefs should stay same')
        self.assertTrue(
            lp.constraints[1].varCoefs[s_1] == np.matrix(np.identity(1)),
            's_0 should have coef of 1-D identity')
        self.assertTrue(
            all(lp.constraints[0].lower == np.array([1, -1]))
            and all(lp.constraints[0].upper >= np.array([1e300])),
            'constraint bounds should remain same')
        self.assertTrue(
            lp.constraints[1].lower == np.array([-2.5])
            and lp.constraints[1].upper >= np.array([1e300]),
            'constraint bounds should remain same')

        # same objective, plus large s coefficient
        self.assertTrue(
            all(lp.objective == np.array([-1, -1, 0, bb._M, bb._M, bb._M])))

        # problem is now feasible
        self.assertTrue(lp.getStatusCode() == 0, 'lp should now be optimal')

    def test_bound_dual_fails_asserts(self):
        bb = BranchAndBound(infeasible)
        bb.solve()
        terminal_nodes = bb.tree.get_leaves(0)
        infeasible_nodes = [
            n for n in terminal_nodes if n.lp_feasible is False
        ]
        n = infeasible_nodes[0]
        n.lp.addVariable('s_0', 1)
        self.assertRaisesRegex(AssertionError,
                               "variable 's_0' is a reserved name",
                               bb._bound_dual, n.lp)
        self.assertRaisesRegex(AssertionError,
                               "must give CyClpSimplex instance",
                               bb._bound_dual, n)

    def test_find_strong_disjunctive_cut_fails_asserts(self):
        bb = BranchAndBound(small_branch)
        bb.solve()

        self.assertRaisesRegex(AssertionError,
                               'parent must already exist in tree',
                               bb.find_strong_disjunctive_cut, 50)

        terminal_nodes = bb.tree.get_leaves(0)
        disjunctive_nodes = [
            n for n in terminal_nodes if n.lp_feasible is not False
        ]
        n = disjunctive_nodes[0]
        n.lp.addVariable('d', 3)

        self.assertRaisesRegex(
            AssertionError,
            'Each disjunctive term should have the same variables',
            bb.find_strong_disjunctive_cut, 0)

    # todo: get confirmation on bound matching
    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()
        # self.assertTrue(bb.global_lower_bound == warm_bb.root_node.objective_value)

    @unittest.skipIf(skip_longs, "debugging")
    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)
コード例 #6
0
 def setUpClass(cls) -> None:
     node = BaseNode(small_branch.lp, small_branch.integerIndices)
     node.bound()
     cls.children = node.branch()