예제 #1
0
    def test_base_branch(self):
        node = BaseNode(small_branch.lp, small_branch.integerIndices,
                        -float('inf'))
        node.bound()
        idx = 2
        rtn = node._base_branch(2)

        # check each node
        for name, n in rtn.items():
            self.assertTrue(
                all(n._lp.matrix.elements == node._lp.matrix.elements))
            self.assertTrue(all(n._lp.objective == node._lp.objective))
            self.assertTrue(
                all(n._lp.constraintsLower == node._lp.constraintsLower))
            self.assertTrue(
                all(n._lp.constraintsUpper == node._lp.constraintsUpper))
            if name == 'down':
                self.assertTrue(all(n._lp.variablesUpper >= [1e10, 1e10, 1]))
                self.assertTrue(n._lp.variablesUpper[idx] == 1)
                self.assertTrue(
                    all(n._lp.variablesLower == node._lp.variablesLower))
            else:
                self.assertTrue(
                    all(n._lp.variablesUpper == node._lp.variablesUpper))
                self.assertTrue(all(n._lp.variablesLower == [0, 0, 2]))
            # check basis statuses work - i.e. are warm started
            for i in [0, 1]:
                self.assertTrue(
                    all(node._lp.getBasisStatus()[i] == n._lp.getBasisStatus()
                        [i]), 'bases should match')
예제 #2
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')
예제 #3
0
 def test_base_bound_infeasible(self):
     node = BaseNode(infeasible.lp, infeasible.integerIndices)
     node._base_bound()
     # infeasible problems should come back as neither lp nor mip feasible
     self.assertFalse(node.lp_feasible)
     self.assertFalse(node.mip_feasible)
     self.assertFalse(node.unbounded)
예제 #4
0
    def test_strong_branch_fails_asserts(self):
        node = BaseNode(small_branch.lp, small_branch.integerIndices, 0)
        node.bound()
        idx = node._most_fractional_index

        # test we stay within iters and improve bound/stay same
        self.assertRaisesRegex(AssertionError, 'iterations must be positive integer',
                               node._strong_branch, idx, 2.5)
예제 #5
0
    def test_eq(self):
        node1 = BaseNode(small_branch.lp, small_branch.integerIndices, 0, -float('inf'))
        node2 = BaseNode(small_branch.lp, small_branch.integerIndices, 0, 0)
        node3 = BaseNode(small_branch.lp, small_branch.integerIndices, 0, 0)

        self.assertTrue(node3 == node2)
        self.assertFalse(node1 == node2)
        self.assertRaises(TypeError, node1.__eq__, 5)
예제 #6
0
 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 == '>=')
예제 #7
0
 def test_variables_nonnegative(self):
     node = BaseNode(small_branch.lp, small_branch.integerIndices, 0)
     self.assertTrue(node._variables_nonnegative)
     node = BaseNode(cut2.lp, cut2.integerIndices, 0)
     l = node.lp.variablesLower.copy()
     l[0] = -10
     node.lp.variablesLower = l
     self.assertFalse(node._variables_nonnegative)
예제 #8
0
 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()
예제 #9
0
 def test_base_bound_infeasible(self):
     node = BaseNode(infeasible.lp, infeasible.integerIndices)
     node._base_bound()
     # infeasible problems should come back as neither lp nor mip feasible
     self.assertFalse(node.lp_feasible)
     self.assertFalse(node.mip_feasible)
     self.assertFalse(node.unbounded)
     self.assertTrue(node.solution is None)
     self.assertTrue(node.objective_value == float('inf'))
예제 #10
0
 def test_base_bound_fractional(self):
     node = BaseNode(small_branch.lp, small_branch.integerIndices)
     node._base_bound()
     self.assertTrue(node.objective_value == -2.75)
     self.assertTrue(all(node.solution == [0, 1.25, 1.5]))
     # fractional solutions should come back as lp but not mip feasible
     self.assertTrue(node.lp_feasible)
     self.assertFalse(node.mip_feasible)
     self.assertFalse(node.unbounded)
예제 #11
0
 def test_base_bound_integer(self):
     node = BaseNode(no_branch.lp, no_branch.integerIndices)
     node._base_bound()
     self.assertTrue(node.objective_value == -2)
     self.assertTrue(all(node.solution == [1, 1, 0]))
     # integer solutions should come back as both lp and mip feasible
     self.assertTrue(node.lp_feasible)
     self.assertTrue(node.mip_feasible)
     self.assertFalse(node.unbounded)
예제 #12
0
    def test_init_lineage(self):
        node = BaseNode(small_branch.lp, small_branch.integerIndices, idx=0)
        self.assertTrue(node.lineage == (0,))

        node = BaseNode(small_branch.lp, small_branch.integerIndices, ancestors=(0,))
        self.assertTrue(node.lineage == (0,))

        node = BaseNode(small_branch.lp, small_branch.integerIndices, idx=3,
                        ancestors=(0, 1))
        self.assertTrue(node.lineage == (0, 1, 3))
예제 #13
0
    def test_lt(self):
        node1 = BaseNode(small_branch.lp, small_branch.integerIndices, 0,  -float('inf'))
        node2 = BaseNode(small_branch.lp, small_branch.integerIndices, 0, 0)

        self.assertTrue(node1 < node2)
        self.assertFalse(node2 < node1)
        self.assertRaises(TypeError, node1.__lt__, 5)

        # make sure if we put them in PQ that they come out in the right order
        q = PriorityQueue()
        q.put(node2)
        q.put(node1)
        self.assertTrue(q.get().lower_bound < 0)
        self.assertTrue(q.get().lower_bound == 0)
예제 #14
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)
 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())
예제 #16
0
    def test_most_fractional_index(self):
        node = BaseNode(no_branch.lp, no_branch.integerIndices, 0)
        node.bound()
        self.assertFalse(node._most_fractional_index,
                         'int solution should have no fractional index')

        node = BaseNode(small_branch.lp, small_branch.integerIndices, 0)
        node.bound()
        self.assertTrue(node._most_fractional_index == 2)
예제 #17
0
    def test_base_branch(self):
        node = BaseNode(small_branch.lp, small_branch.integerIndices)
        node.bound()
        idx = 2
        out = {next_node_idx: node._base_branch(2, next_node_idx) for
               next_node_idx in [None, 1]}

        # confirm current node is no longer a leaf
        self.assertFalse(node.is_leaf)

        # check each node
        for next_node_idx, rtn in out.items():
            for name, n in rtn.items():
                if name not in ['left', 'right']:
                    continue
                self.assertTrue(all(n.lp.matrix.elements == node.lp.matrix.elements))
                self.assertTrue(all(n.lp.objective == node.lp.objective))
                self.assertTrue(all(n.lp.constraintsLower == node.lp.constraintsLower))
                self.assertTrue(all(n.lp.constraintsUpper == node.lp.constraintsUpper))
                self.assertTrue(n._integer_indices == node._integer_indices)
                self.assertTrue(n.lower_bound == node.objective_value)
                self.assertTrue(n._b_idx == idx)
                self.assertTrue(n._b_val == 1.5)
                self.assertTrue(n.depth == 1)
                if name == 'left':
                    self.assertTrue(all(n.lp.variablesUpper == [10, 10, 1]))
                    self.assertTrue(n.lp.variablesUpper[idx] == 1)
                    self.assertTrue(all(n.lp.variablesLower == node.lp.variablesLower))
                    self.assertTrue(n.lineage == (1, ) if next_node_idx else n.lineage is None)
                    self.assertTrue(n.idx == 1 if next_node_idx else n.idx is None)
                    self.assertTrue(n._b_dir == 'left')
                else:
                    self.assertTrue(all(n.lp.variablesUpper == node.lp.variablesUpper))
                    self.assertTrue(all(n.lp.variablesLower == [0, 0, 2]))
                    self.assertTrue(n.lineage == (2,) if next_node_idx else n.lineage is None)
                    self.assertTrue(n.idx == 2 if next_node_idx else n.idx is None)
                    self.assertTrue(n._b_dir == 'right')
                # check basis statuses work - i.e. are warm started
                for i in [0, 1]:
                    self.assertTrue(all(node.lp.getBasisStatus()[i] ==
                                        n.lp.getBasisStatus()[i]), 'bases should match')

            # check other returns
            self.assertTrue(rtn['next_node_idx'] == 3 if next_node_idx else
                            rtn['next_node_idx'] is None)
예제 #18
0
    def test_base_branch_fails_asserts(self):
        # branching before solving should fail
        node = BaseNode(no_branch.lp, no_branch.integerIndices)
        self.assertRaisesRegex(AssertionError,
                               'must solve before branching',
                               node._base_branch,
                               idx=0)

        # branching on integer feasible node should fail
        node.bound()
        self.assertRaisesRegex(AssertionError,
                               'must branch on integer index',
                               node._base_branch,
                               idx=-1)

        # branching on non integer index should fail
        self.assertRaisesRegex(AssertionError,
                               'index branched on must be fractional',
                               node._base_branch,
                               idx=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')
예제 #20
0
    def test_bound(self):
        # check function calls
        node = BaseNode(infeasible.lp, infeasible.integerIndices)
        with patch.object(node, '_base_bound') as bb:
            node.bound(junk='stuff')  # should work with extra args
            self.assertTrue(bb.call_count == 1, 'should call base bound')

        # check return
        node = BaseNode(infeasible.lp, infeasible.integerIndices)
        rtn = node.bound()
        self.assertTrue(isinstance(rtn, dict), 'should return dict')
        self.assertFalse(rtn, 'dict should be empty')
예제 #21
0
 def test_init(self):
     node = BaseNode(small_branch.lp, small_branch.integerIndices)
     self.assertTrue(node._lp, 'should get a model on proper instantiation')
     self.assertTrue(node.lower_bound == -float('inf'))
     self.assertFalse(node.objective_value, 'should have obj but empty')
     self.assertFalse(node.solution, 'should have solution but empty')
     self.assertFalse(node.lp_feasible, 'should have lp_feasible but empty')
     self.assertFalse(node.lp_feasible, 'should have unbounded but empty')
     self.assertFalse(node.mip_feasible,
                      'should have mip_feasible but empty')
     self.assertTrue(node._epsilon > 0, 'should have epsilon > 0')
     self.assertFalse(node._b_dir, 'should have branch direction but empty')
     self.assertFalse(node._b_idx, 'should have branch index but empty')
     self.assertFalse(node._b_val, 'should have node value but empty')
     self.assertFalse(node.depth, 'should have depth but 0')
     self.assertTrue(node.branch_method == 'most fractional')
     self.assertTrue(node.search_method == 'best first')
예제 #22
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()))
예제 #23
0
 def test_init(self):
     node = BaseNode(small_branch.lp, small_branch.integerIndices)
     self.assertTrue(node.lp, 'should get a model on proper instantiation')
     self.assertTrue(node._integer_indices == [0, 1, 2], 'should have list of integer indices')
     self.assertFalse(node.idx, 'idx should be None')
     self.assertTrue(node._var_indices == list(range(3)), 'should have list of var indices')
     self.assertTrue(node._row_indices == list(range(2)), 'should have list of row indices')
     self.assertTrue(node.lower_bound == -float('inf'))
     self.assertFalse(node.objective_value, 'should have obj but empty')
     self.assertFalse(node.solution, 'should have solution but empty')
     self.assertFalse(node.lp_feasible, 'should have lp_feasible but empty')
     self.assertFalse(node.unbounded, 'should have unbounded but empty')
     self.assertFalse(node.mip_feasible, 'should have mip_feasible but empty')
     self.assertTrue(node._epsilon > 0, 'should have epsilon > 0')
     self.assertFalse(node._b_dir, 'should have branch direction but empty')
     self.assertFalse(node._b_idx, 'should have branch index but empty')
     self.assertFalse(node._b_val, 'should have node value but empty')
     self.assertFalse(node.depth, 'should have depth but 0')
     self.assertTrue(node.branch_method == 'most fractional')
     self.assertTrue(node.search_method == 'best first')
     self.assertTrue(node.is_leaf, 'all nodes instantiate to being leaves')
     self.assertFalse(node.lineage, 'lineage should be None')
예제 #24
0
 def test_is_fractional(self):
     node = BaseNode(small_branch.lp, small_branch.integerIndices, 0)
     self.assertTrue(node._is_fractional(5.5))
     self.assertFalse(node._is_fractional(5))
     self.assertFalse(node._is_fractional(5.999999))
     self.assertFalse(node._is_fractional(5.000001))
예제 #25
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)
예제 #26
0
    def test_base_bound_unbounded(self):
        node = BaseNode(unbounded.lp, unbounded.integerIndices)
        node._base_bound()

        self.assertTrue(node.lp_feasible)
        self.assertTrue(node.unbounded)
예제 #27
0
 def test_sense_fails_asserts(self):
     node = BaseNode(small_branch.lp, small_branch.integerIndices, 0)
     node.lp.addConstraint(-CyLPArray([1, 0, 0]) * node.lp.getVarByName('x') >= 1)
     with self.assertRaises(AssertionError):
         node._sense
예제 #28
0
    def test_strong_branch(self):
        iters = 5
        node = BaseNode(random.lp, random.integerIndices, 0)
        node.bound()
        idx = node._most_fractional_index

        # test we stay within iters and improve bound/stay same
        rtn = node._strong_branch(idx, iterations=iters)
        for direction, child_node in rtn.items():
            self.assertTrue(child_node.lp.iteration <= iters)
            if child_node.lp.getStatusCode() in [0, 3]:
                self.assertTrue(child_node.lp.objectiveValue >= node.objective_value)

        # test call base_branch
        node = BaseNode(random.lp, random.integerIndices)
        node.bound()
        idx = node._most_fractional_index
        children = node._base_branch(idx)
        with patch.object(node, '_base_branch') as bb:
            bb.return_value = children
            rtn = node._strong_branch(idx, iterations=iters)
            self.assertTrue(bb.called)
예제 #29
0
 def test_get_fraction(self):
     node = BaseNode(small_branch.lp, small_branch.integerIndices, 0)
     self.assertTrue(.5 == node._get_fraction(5.5))
예제 #30
0
 def test_get_fraction_fails_asserts(self):
     node = BaseNode(small_branch.lp, small_branch.integerIndices, 0)
     self.assertRaisesRegex(AssertionError, 'value should be a number',
                            node._get_fraction, '5.5')