コード例 #1
0
ファイル: cutting_plane.py プロジェクト: abdxyz/mathu-solver
    def _find_split_inequality(self: G, idx: int, **kwargs: Any):
        assert idx in self._integer_indices, 'must lift and project on integer index'
        x_idx = self.solution[idx]
        assert self._is_fractional(
            x_idx), "must lift and project on index with fractional value"

        # build the CGLP model from ISE 418 Lecture 15 Slide 7 but for LP with >= constraints
        lp = CyClpSimplex()

        # declare variables
        pi = lp.addVariable('pi', self.lp.nVariables)
        pi0 = lp.addVariable('pi0', 1)
        u1 = lp.addVariable('u1', self.lp.nConstraints)
        u2 = lp.addVariable('u2', self.lp.nConstraints)
        w1 = lp.addVariable('w1', self.lp.nVariables)
        w2 = lp.addVariable('w2', self.lp.nVariables)

        # set bounds
        lp += u1 >= CyLPArray(np.zeros(self.lp.nConstraints))
        lp += u2 >= CyLPArray(np.zeros(self.lp.nConstraints))

        w_ub = CyLPArray(np.zeros(self.lp.nVariables))
        w_ub[idx] = float('inf')
        lp += w_ub >= w1 >= CyLPArray(np.zeros(self.lp.nVariables))
        lp += w_ub >= w2 >= CyLPArray(np.zeros(self.lp.nVariables))

        # set constraints
        # (pi, pi0) must be valid for both parts of the disjunction
        lp += 0 >= -pi + self.lp.coefMatrix.T * u1 - w1
        lp += 0 >= -pi + self.lp.coefMatrix.T * u2 + w2
        lp += 0 <= -pi0 + CyLPArray(
            self.lp.constraintsLower) * u1 - floor(x_idx) * w1.sum()
        lp += 0 <= -pi0 + CyLPArray(
            self.lp.constraintsLower) * u2 + ceil(x_idx) * w2.sum()
        # normalize variables
        lp += u1.sum() + u2.sum() + w1.sum() + w2.sum() == 1

        # set objective: find the deepest cut
        # since pi * x >= pi0 for all x in disjunction, we want min pi * x_star - pi0
        lp.objective = CyLPArray(self.solution) * pi - pi0

        # solve
        lp.primal(startFinishOptions='x')
        assert lp.getStatusCode() == 0, 'we should get optimal solution'
        assert lp.objectiveValue <= 0, 'pi * x >= pi -> pi * x - pi >= 0 -> ' \
                                       'negative objective at x^* since it gets cut off'

        # get solution
        return lp.primalVariableSolution['pi'], lp.primalVariableSolution[
            'pi0']
コード例 #2
0
    def find_strong_disjunctive_cut(self, root_id: int) -> Tuple[CyLPArray, float]:
        """ Generate a strong cut valid for the disjunction encoded in the subtree
        rooted at node <root_id>. This cut is optimized to maximize the violation
        of the LP relaxation solution at node <root_id>

        see ISE 418 Lecture 13 slide 3, Lecture 14 slide 9, and Lecture 15 slides
        6-7 for derivation

        :param root_id: id of the node off which we will base the disjunction
        :return: a valid inequality (pi, pi0), i.e. pi^T x >= pi0 for all x in
        the convex hull of the disjunctive terms' LP relaxations
        """
        # sanity checks
        assert root_id in self.tree, 'parent must already exist in tree'
        root = self.tree.get_node_instances(root_id)
        # get each disjunctive term
        terminal_nodes = self.tree.get_leaves(root_id)
        # terminal nodes pruned for infeasibility do not expand disjunction, so remove them
        disjunctive_nodes = {n.idx: n for n in terminal_nodes if n.lp_feasible is not False}
        var_dicts = [{v.name: v.dim for v in n.lp.variables} for n in disjunctive_nodes.values()]
        assert all(var_dicts[0] == d for d in var_dicts), \
            'Each disjunctive term should have the same variables. The feature allowing' \
            ' otherwise remains to be developed.'

        # useful constants
        num_vars = sum(var_dim for var_dim in var_dicts[0].values())
        inf = root.lp.getCoinInfinity()

        # set infinite lower/upper bounds to 0 so they don't create numerical issues in constraints
        lb = {idx: CyLPArray([val if val > -inf else 0 for val in n.lp.variablesLower])
              for idx, n in disjunctive_nodes.items()}  # adjusted lower bound
        ub = {idx: CyLPArray([val if val < inf else 0 for val in n.lp.variablesUpper])
              for idx, n in disjunctive_nodes.items()}  # adjusted upper bound

        # set corresponding variables in cglp to 0 to reflect there is no bound
        # i.e. this variable should not exist in cglp
        wb = {idx: CyLPArray([inf if val > -inf else 0 for val in n.lp.variablesLower])
              for idx, n in disjunctive_nodes.items()}  # w bounds - variable on lb constraints
        vb = {idx: CyLPArray([inf if val < inf else 0 for val in n.lp.variablesUpper])
              for idx, n in disjunctive_nodes.items()}  # v bounds - variable on ub constraints

        # instantiate LP
        cglp = CyClpSimplex()
        cglp.logLevel = 0  # quiet output when resolving

        # declare variables (what to do with case when we have degenerate constraint)
        pi = cglp.addVariable('pi', num_vars)
        pi0 = cglp.addVariable('pi0', 1)
        u = {idx: cglp.addVariable(f'u_{idx}', n.lp.nConstraints) for idx, n in
             disjunctive_nodes.items()}
        w = {idx: cglp.addVariable(f'w_{idx}', n.lp.nVariables) for idx, n in
             disjunctive_nodes.items()}
        v = {idx: cglp.addVariable(f'v_{idx}', n.lp.nVariables) for idx, n in
             disjunctive_nodes.items()}

        # bound them
        for idx in disjunctive_nodes:
            cglp += u[idx] >= 0
            cglp += 0 <= w[idx] <= wb[idx]
            cglp += 0 <= v[idx] <= vb[idx]

        # add constraints
        for i, n in disjunctive_nodes.items():
            # (pi, pi0) must be valid for each disjunctive term's LP relaxation
            cglp += 0 >= -pi + n.lp.coefMatrix.T * u[i] + \
                np.matrix(np.eye(num_vars)) * w[i] - np.matrix(np.eye(num_vars)) * v[i]
            cglp += 0 <= -pi0 + CyLPArray(n.lp.constraintsLower) * u[i] + \
                lb[i] * w[i] - ub[i] * v[i]
        # normalize variables so they don't grow arbitrarily
        cglp += sum(var.sum() for var_dict in [u, w, v] for var in var_dict.values()) == 1

        # set objective: find the deepest cut
        # since pi * x >= pi0 for all x in disjunction, we want min pi * x_star - pi0
        cglp.objective = CyLPArray(root.solution) * pi - pi0

        # solve
        cglp.primal(startFinishOptions='x')
        assert cglp.getStatusCode() == 0, 'we should get optimal solution'
        assert cglp.objectiveValue <= 0, 'pi * x >= pi0 -> pi * x - pi0 >= 0 -> ' \
            'negative objective at x^* since it gets cut off'

        # get solution
        return cglp.primalVariableSolution['pi'], cglp.primalVariableSolution['pi0']
コード例 #3
0
def BranchAndBoundCylp(T, CONSTRAINTS, VARIABLES, OBJ, MAT, RHS,
                   branch_strategy=MOST_FRACTIONAL,
                   search_strategy=DEPTH_FIRST,
                   complete_enumeration=False,
                   display_interval=None,
                   binary_vars=True):
    if T.get_layout() == 'dot2tex':
        cluster_attrs = {'name':'Key', 'label':r'\text{Key}', 'fontsize':'12'}
        T.add_node('C', label = r'\text{Candidate}', style = 'filled',
                      color = 'yellow', fillcolor = 'yellow')
        T.add_node('I', label = r'\text{Infeasible}', style = 'filled',
                      color = 'orange', fillcolor = 'orange')
        T.add_node('S', label = r'\text{Solution}', style = 'filled',
                      color = 'lightblue', fillcolor = 'lightblue')
        T.add_node('P', label = r'\text{Pruned}', style = 'filled',
                      color = 'red', fillcolor = 'red')
        T.add_node('PC', label = r'\text{Pruned}$\\ $\text{Candidate}', style = 'filled',
                      color = 'red', fillcolor = 'yellow')
    else:
        cluster_attrs = {'name':'Key', 'label':'Key', 'fontsize':'12'}
        T.add_node('C', label = 'Candidate', style = 'filled',
                      color = 'yellow', fillcolor = 'yellow')
        T.add_node('I', label = 'Infeasible', style = 'filled',
                      color = 'orange', fillcolor = 'orange')
        T.add_node('S', label = 'Solution', style = 'filled',
                      color = 'lightblue', fillcolor = 'lightblue')
        T.add_node('P', label = 'Pruned', style = 'filled',
                      color = 'red', fillcolor = 'red')
        T.add_node('PC', label = 'Pruned \n Candidate', style = 'filled',
                      color = 'red', fillcolor = 'yellow')
    T.add_edge('C', 'I', style = 'invisible', arrowhead = 'none')
    T.add_edge('I', 'S', style = 'invisible', arrowhead = 'none')
    T.add_edge('S', 'P', style = 'invisible', arrowhead = 'none')
    T.add_edge('P', 'PC', style = 'invisible', arrowhead = 'none')
    T.create_cluster(['C', 'I', 'S', 'P', 'PC'], cluster_attrs)

    # Change to CyLP format
    cyOBJ = CyLPArray([-val for val in OBJ.values()]) # CyLP takes min
    cyMAT = np.matrix([MAT[v] for v in VARIABLES]).T
    cyRHS = CyLPArray(RHS)


    # The initial lower bound
    LB = -INFINITY
    # The number of LP's solved, and the number of nodes solved
    node_count = 1
    iter_count = 0
    lp_count = 0

    numCons = len(CONSTRAINTS)
    numVars = len(VARIABLES)
    # List of incumbent solution variable values
    opt = dict([(i, 0) for i in VARIABLES])
    pseudo_u = dict((i, (OBJ[i], 0)) for i in VARIABLES)
    pseudo_d = dict((i, (OBJ[i], 0)) for i in VARIABLES)
    print("===========================================")
    print("Starting Branch and Bound")
    if branch_strategy == MOST_FRACTIONAL:
        print("Most fractional variable")
    elif branch_strategy == FIXED_BRANCHING:
        print("Fixed order")
    elif branch_strategy == PSEUDOCOST_BRANCHING:
        print("Pseudocost brancing")
    else:
        print("Unknown branching strategy %s" %branch_strategy)
    if search_strategy == DEPTH_FIRST:
        print("Depth first search strategy")
    elif search_strategy == BEST_FIRST:
        print("Best first search strategy")
    else:
        print("Unknown search strategy %s" %search_strategy)
    print("===========================================")
    # List of candidate nodes
    Q = PriorityQueue()
    # The current tree depth
    cur_depth = 0
    cur_index = 0
    # Timer
    timer = time.time()
    Q.push(0, -INFINITY, (0, None, None, None, None, None, None))
    # Branch and Bound Loop
    while not Q.isEmpty():
        infeasible = False
        integer_solution = False
        (cur_index, parent, relax, branch_var, branch_var_value, sense,
        rhs) = Q.pop()
        if cur_index is not 0:
            cur_depth = T.get_node_attr(parent, 'level') + 1
        else:
            cur_depth = 0
        print("")
        print("----------------------------------------------------")
        print("")
        if LB > -INFINITY:
            print("Node: %s, Depth: %s, LB: %s" %(cur_index,cur_depth,LB))
        else:
            print("Node: %s, Depth: %s, LB: %s" %(cur_index,cur_depth,"None"))
        if relax is not None and relax <= LB:
            print("Node pruned immediately by bound")
            T.set_node_attr(parent, 'color', 'red')
            continue
        #====================================
        #    LP Relaxation
        #====================================
        # Compute lower bound by LP relaxation
        prob = CyLPModel()

        # CyLP do not allow change variable object without model
        if binary_vars:
            var = prob.addVariable('x', dim=len(VARIABLES))
            prob += 0 <= var <= 1
        else:
            var = prob.addVariable('x', dim=len(VARIABLES))
            prob += 0 <= var # To avoid random generated problems be unbounded


        prob += cyMAT * var <= RHS
        prob.objective = cyOBJ * var
        s = CyClpSimplex(prob)

        # Fix all prescribed variables
        branch_vars = []
        if cur_index is not 0:
            sys.stdout.write("Branching variables: ")
            branch_vars.append(branch_var)
            if sense == '>=':
                prob += var[int(branch_var[1:])] >= rhs # slice the name for index
            else:
                prob += var[int(branch_var[1:])] <= rhs # slice the name for index
            print(branch_var, end=' ')
            pred = parent
            while not str(pred) == '0':
                pred_branch_var = T.get_node_attr(pred, 'branch_var')
                pred_rhs = T.get_node_attr(pred, 'rhs')
                pred_sense = T.get_node_attr(pred, 'sense')
                if pred_sense == '<=':
                    prob += var[int(pred_branch_var[1:])] <= pred_rhs
                else:
                    prob += var[int(pred_branch_var[1:])] >= pred_rhs
                print(pred_branch_var, end=' ')
                branch_vars.append(pred_branch_var)
                pred = T.get_node_attr(pred, 'parent')
            print()
        # Solve the LP relaxation
        s = CyClpSimplex(prob)
        s.initialSolve()
        lp_count = lp_count +1

        if (s.getStatusCode() < 0):
            print('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%')
            print(lp_count)
            print(s.getStatusCode())
            print(s.primal())
            print(s.objectiveValue)
            print(s.primalVariableSolution)
            print('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%')

        # Check infeasibility
        #-1 - unknown e.g. before solve or if postSolve says not optimal
        #0 - optimal
        #1 - primal infeasible
        #2 - dual infeasible
        #3 - stopped on iterations or time
        #4 - stopped due to errors
        #5 - stopped by event handler (virtual int ClpEventHandler::event())
        infeasible = (s.getStatusCode() in [-1,1,3,4,5])
        # Print status
        if infeasible:
            print("LP Solved, status: Infeasible")
        else:
            print("LP Solved, status: %s, obj: %s" %(s.getStatusString(),-s.objectiveValue))

        if(s.getStatusCode() == 0):
            relax = -s.objectiveValue
            # Update pseudocost
            if branch_var != None:
                if sense == '<=':
                    pseudo_d[branch_var] = (
                    old_div((pseudo_d[branch_var][0]*pseudo_d[branch_var][1] +
                    old_div((T.get_node_attr(parent, 'obj') - relax),
                    (branch_var_value - rhs))),(pseudo_d[branch_var][1]+1)),
                    pseudo_d[branch_var][1]+1)
                else:
                    pseudo_u[branch_var] = (
                    old_div((pseudo_u[branch_var][0]*pseudo_d[branch_var][1] +
                     old_div((T.get_node_attr(parent, 'obj') - relax),
                     (rhs - branch_var_value))),(pseudo_u[branch_var][1]+1)),
                    pseudo_u[branch_var][1]+1)
            var_values = dict([(i, s.primalVariableSolution['x'][int(i[1:])]) for i in VARIABLES])
            integer_solution = 1
            for i in VARIABLES:
                if (abs(round(var_values[i]) - var_values[i]) > .001):
                    integer_solution = 0
                    break
            # Determine integer_infeasibility_count and
            # Integer_infeasibility_sum for scatterplot and such
            integer_infeasibility_count = 0
            integer_infeasibility_sum = 0.0
            for i in VARIABLES:
                if (var_values[i] not in set([0,1])):
                    integer_infeasibility_count += 1
                    integer_infeasibility_sum += min([var_values[i],
                                                      1.0-var_values[i]])
            if (integer_solution and relax>LB):
                LB = relax
                for i in VARIABLES:
                    # These two have different data structures first one
                    #list, second one dictionary
                    opt[i] = var_values[i]
                print("New best solution found, objective: %s" %relax)
                for i in VARIABLES:
                    if var_values[i] > 0:
                        print("%s = %s" %(i, var_values[i]))
            elif (integer_solution and relax<=LB):
                print("New integer solution found, objective: %s" %relax)
                for i in VARIABLES:
                    if var_values[i] > 0:
                        print("%s = %s" %(i, var_values[i]))
            else:
                print("Fractional solution:")
                for i in VARIABLES:
                    if var_values[i] > 0:
                        print("%s = %s" %(i, var_values[i]))
            #For complete enumeration
            if complete_enumeration:
                relax = LB - 1
        else:
            relax = INFINITY
        if integer_solution:
            print("Integer solution")
            BBstatus = 'S'
            status = 'integer'
            color = 'lightblue'
        elif infeasible:
            print("Infeasible node")
            BBstatus = 'I'
            status = 'infeasible'
            color = 'orange'
        elif not complete_enumeration and relax <= LB:
            print("Node pruned by bound (obj: %s, UB: %s)" %(relax,LB))
            BBstatus = 'P'
            status = 'fathomed'
            color = 'red'
        elif cur_depth >= numVars :
            print("Reached a leaf")
            BBstatus = 'fathomed'
            status = 'L'
        else:
            BBstatus = 'C'
            status = 'candidate'
            color = 'yellow'
        if BBstatus is 'I':
            if T.get_layout() == 'dot2tex':
                label = '\text{I}'
            else:
                label = 'I'
        else:
            label = "%.1f"%relax
        if iter_count == 0:
            if status is not 'candidate':
                integer_infeasibility_count = None
                integer_infeasibility_sum = None
            if status is 'fathomed':
                if T._incumbent_value is None:
                    print('WARNING: Encountered "fathom" line before '+\
                        'first incumbent.')
            T.AddOrUpdateNode(0, None, None, 'candidate', relax,
                             integer_infeasibility_count,
                             integer_infeasibility_sum,
                             label = label,
                             obj = relax, color = color,
                             style = 'filled', fillcolor = color)
            if status is 'integer':
                T._previous_incumbent_value = T._incumbent_value
                T._incumbent_value = relax
                T._incumbent_parent = -1
                T._new_integer_solution = True
    #           #Currently broken
    #           if ETREE_INSTALLED and T.attr['display'] == 'svg':
    #               T.write_as_svg(filename = "node%d" % iter_count,
    #                                 nextfile = "node%d" % (iter_count + 1),
    #                                 highlight = cur_index)
        else:
            _direction = {'<=':'L', '>=':'R'}
            if status is 'infeasible':
                integer_infeasibility_count = T.get_node_attr(parent,
                                     'integer_infeasibility_count')
                integer_infeasibility_sum = T.get_node_attr(parent,
                                     'integer_infeasibility_sum')
                relax = T.get_node_attr(parent, 'lp_bound')
            elif status is 'fathomed':
                if T._incumbent_value is None:
                    print('WARNING: Encountered "fathom" line before'+\
                        ' first incumbent.')
                    print('  This may indicate an error in the input file.')
            elif status is 'integer':
                integer_infeasibility_count = None
                integer_infeasibility_sum = None
            T.AddOrUpdateNode(cur_index, parent, _direction[sense],\
                                 status, relax,\
                                 integer_infeasibility_count,\
                                 integer_infeasibility_sum,\
                                 branch_var = branch_var,\
                                 branch_var_value = var_values[branch_var],\
                                 sense = sense, rhs = rhs, obj = relax,\
                                 color = color, style = 'filled',\
                                 label = label, fillcolor = color)
            if status is 'integer':
                T._previous_incumbent_value = T._incumbent_value
                T._incumbent_value = relax
                T._incumbent_parent = parent
                T._new_integer_solution = True
            # Currently Broken
    #           if ETREE_INSTALLED and T.attr['display'] == 'svg':
    #               T.write_as_svg(filename = "node%d" % iter_count,
    #                                 prevfile = "node%d" % (iter_count - 1),
    #                                 nextfile = "node%d" % (iter_count + 1),
    #                                 highlight = cur_index)
            if T.get_layout() == 'dot2tex':
                _dot2tex_label = {'>=':' \geq ', '<=':' \leq '}
                T.set_edge_attr(parent, cur_index, 'label',\
                                   str(branch_var) + _dot2tex_label[sense] +\
                                   str(rhs))
            else:
                T.set_edge_attr(parent, cur_index, 'label',\
                                   str(branch_var) + sense + str(rhs))
        iter_count += 1
        if BBstatus == 'C':
            # Branching:
            # Choose a variable for branching
            branching_var = None
            if branch_strategy == FIXED_BRANCHING:
                #fixed order
                for i in VARIABLES:
                    frac = min(s.primalVariableSolution['x'][int(i[1:])]-math.floor(s.primalVariableSolution['x'][int(i[1:])]),\
                               math.ceil(s.primalVariableSolution['x'][int(i[1:])]) - s.primalVariableSolution['x'][int(i[1:])])
                    if (frac > 0):
                        min_frac = frac
                        branching_var = i
                        # TODO(aykut): understand this break
                        break
            elif branch_strategy == MOST_FRACTIONAL:
                #most fractional variable
                min_frac = -1
                for i in VARIABLES:
                    frac = min(s.primalVariableSolution['x'][int(i[1:])]-math.floor(s.primalVariableSolution['x'][int(i[1:])]),\
                               math.ceil(s.primalVariableSolution['x'][int(i[1:])])- s.primalVariableSolution['x'][int(i[1:])])
                    if (frac> min_frac):
                        min_frac = frac
                        branching_var = i
            elif branch_strategy == PSEUDOCOST_BRANCHING:
                scores = {}
                for i in VARIABLES:
                    # find the fractional solutions
                    if (s.primalVariableSolution['x'][int(i[1:])] - math.floor(s.primalVariableSolution['x'][int(i[1:])])) != 0:
                        scores[i] = min(pseudo_u[i][0]*(1-s.primalVariableSolution['x'][int(i[1:])]),\
                                        pseudo_d[i][0]*s.primalVariableSolution['x'][int(i[1:])])
                    # sort the dictionary by value
                branching_var = sorted(list(scores.items()),
                                       key=lambda x : x[1])[-1][0]
            else:
                print("Unknown branching strategy %s" %branch_strategy)
                exit()
            if branching_var is not None:
                print("Branching on variable %s" %branching_var)
            #Create new nodes
            if search_strategy == DEPTH_FIRST:
                priority = (-cur_depth - 1, -cur_depth - 1)
            elif search_strategy == BEST_FIRST:
                priority = (-relax, -relax)
            elif search_strategy == BEST_ESTIMATE:
                priority = (-relax - pseudo_d[branching_var][0]*\
                                 (math.floor(s.primalVariableSolution['x'][int(branching_var[1:])]) -\
                                      s.primalVariableSolution['x'][int(branching_var[1:])]),\
                            -relax + pseudo_u[branching_var][0]*\
                                 (math.ceil(s.primalVariableSolution['x'][int(branching_var[1:])]) -\
                                      s.primalVariableSolution['x'][int(branching_var[1:])]))
            node_count += 1
            Q.push(node_count, priority[0], (node_count, cur_index, relax, branching_var,
                    var_values[branching_var],
                    '<=', math.floor(s.primalVariableSolution['x'][int(branching_var[1:])])))
            node_count += 1
            Q.push(node_count, priority[1], (node_count, cur_index, relax, branching_var,
                    var_values[branching_var],
                    '>=', math.ceil(s.primalVariableSolution['x'][int(branching_var[1:])])))
            T.set_node_attr(cur_index, color, 'green')
        if T.root is not None and display_interval is not None and\
                iter_count%display_interval == 0:
            T.display(count=iter_count)

    timer = int(math.ceil((time.time()-timer)*1000))
    print("")
    print("===========================================")
    print("Branch and bound completed in %sms" %timer)
    print("Strategy: %s" %branch_strategy)
    if complete_enumeration:
        print("Complete enumeration")
    print("%s nodes visited " %node_count)
    print("%s LP's solved" %lp_count)
    print("===========================================")
    print("Optimal solution")
    #print optimal solution
    for i in sorted(VARIABLES):
        if opt[i] > 0:
            print("%s = %s" %(i, opt[i]))
    print("Objective function value")
    print(LB)
    print("===========================================")
    if T.attr['display'] is not 'off':
        T.display(count=iter_count)
    T._lp_count = lp_count
    return LB,opt