Decision Optimization

Decision Optimization

Delivers prescriptive analytics capabilities and decision intelligence to improve decision-making.

 View Only
  • 1.  Docplex Python "Losing Constraint"

    Posted Tue May 12, 2020 06:13 AM

    Hi everyone,

    I am working on a cutting plane algorithm using CPLEX on Python. The idea is something like: in each iteration, I take all the previous solutions from previous iterations that were violating a relaxation that I had and create a set of mixed-integer cuts. Those cuts are added into a model and the model is solved again. To make things more clear for me, in each iteration I build the model from scratch. In order to understand what's happening in that loop, I keep track of the number of constraints that are added by each iteration. However, after a couple of iterations, I observed that even though new constraints were required and properly calculated, the model is somehow "losing" some of them on the way. 

    For example, at iteration 3 there are  1312 constraints. However, in iteration 4, even after adding one more cut, the number of constraints dropped to 1273.

    > ITERATION 3
    Adding cut in y
    Adding cut in y
    Adding cut in y

    Model: Action
    - number of variables: 712
    - binary=212, integer=0, continuous=500
    - number of constraints: 1312
    - linear=1272, quadratic=40
    - parameters:
    parameters.threads = 1
    parameters.timelimit = 1000.00000000000000
    parameters.mip.display = 1
    - problem type is: MIQCP

     

    > ITERATION 4
    Adding cut in y
    Model: Action
    - number of variables: 714
    - binary=214, integer=0, continuous=500
    - number of constraints: 1273
    - linear=1233, quadratic=40
    - parameters:
    parameters.threads = 1
    parameters.timelimit = 1000.00000000000000
    parameters.mip.display = 1
    - problem type is: MIQCP


    Has anyone faced something similar?
    Any help would be highly appreciated!

    Thank you very much




    ------------------------------
    Fernando Dias
    ------------------------------

    #DecisionOptimization


  • 2.  RE: Docplex Python "Losing Constraint"

    Posted Tue May 12, 2020 09:38 AM
    How do you cuts look like? Are these just linear constraints? From your output you not only add constraints but also add variables. Could you export your model using Model.export_as_lp() for each iteration and then compare the LP files generated for each iteration? What is the difference between them? Can you maybe attach the two files here?

    ------------------------------
    Daniel Junglas
    ------------------------------



  • 3.  RE: Docplex Python "Losing Constraint"

    Posted Wed May 13, 2020 12:11 AM

    Hey Daniel

    I just attached the code, an instance for testing that is causing this problem and the exported model for the first 3 iterations. And yes, I add constraints and variable in each iteration. 

    Thanks for your help



    ------------------------------
    Fernando Dias
    ------------------------------



  • 4.  RE: Docplex Python "Losing Constraint"

    Posted Wed May 13, 2020 04:03 AM
    Not sure I understand your code correctly but you have code like this:
    if abs(tdx_prev[i] - dx_prev[i]**2) > tol and q_s[i] < qmin - tol:
    which seems to control whether a cut is added for a particular i in A. So a cut is only added if it is violated by the current solution? If that is correct then it could well be that a cut is violated in one iteration but not in one of the sub-sequent iterations. That could explain that a different set of cuts is used in each iteration?

    ------------------------------
    Daniel Junglas
    ------------------------------



  • 5.  RE: Docplex Python "Losing Constraint"

    Posted Sun May 24, 2020 11:42 PM

    Thank you very much for your help

    It was exactly the problem that I had.

    Now furthering that, I wanna use that loop that I have that creates the cuts as a separated function and then use that function as part of a callback option in the DOCPLEX library (if they're available). Basically, the idea is to instead of solving the model from start in every iteration, I would start the code as it is, and from time to time, check if the cuts are necessary and add them, without stopping, going another iteration in the loop or rebuilding the model. So the callbacks would verify if cuts are necessary, process them and add them to the original model.

    Any idea if that's possible in Python using the docplex library and how to start it?

    Thank you



    ------------------------------
    Fernando Dias
    ------------------------------



  • 6.  RE: Docplex Python "Losing Constraint"

    Posted Mon May 25, 2020 12:46 AM
    Here is a short code example that uses a cut callback with CPLEX. In order to do this, you have to use the (undocumented) mixin class:
    # ---------------------------------------------------------------------------
    # File: cut_callback.py
    # ---------------------------------------------------------------------------
    # Licensed Materials - Property of IBM
    # 5725-A06 5725-A29 5724-Y48 5724-Y49 5724-Y54 5724-Y55 5655-Y21
    # Copyright IBM Corporation 2009, 2017. All Rights Reserved.
    #
    # US Government Users Restricted Rights - Use, duplication or
    # disclosure restricted by GSA ADP Schedule Contract with
    # IBM Corp.
    # ---------------------------------------------------------------------------
    #
    # Given a set of locations J and a set of clients C
    #  Minimize
    #   sum(j in J) fixedCost[j]*used[j] +
    #   sum(j in J)sum(c in C)cost[c][j]*supply[c][j]
    #  Subject to
    #   sum(j in J) supply[c][j] == 1                    for all c in C
    #   sum(c in C) supply[c][j] <= (|C| - 1) * used[j]  for all j in J
    #               supply[c][j] in { 0, 1 }             for all c in C, j in J
    #                    used[j] in { 0, 1 }             for all j in J
    
    
    import sys
    
    from cplex.callbacks import UserCutCallback
    
    from docplex.mp.callbacks.cb_mixin import *
    from docplex.mp.model import Model
    
    # Separate the disaggregated capacity constraints.
    # In the model we have for each location j the constraint
    #    sum(c in clients) supply[c][j] <= (nbClients-1) * used[j]
    # Clearly, a client can only be serviced from a location that is used,
    # so we also have a constraint
    #    supply[c][j] <= used[j]
    # that must be satisfied by every feasible solution. These constraints tend
    # to be violated in LP relaxation. In this callback we separate them in cuts
    # constraints added via a callback.
    
    
    class MyCutCallback(ConstraintCallbackMixin, UserCutCallback):
        # Callback constructor. Model object is set after registration.
        def __init__(self, env):
            UserCutCallback.__init__(self, env)
            ConstraintCallbackMixin.__init__(self)
            self.eps = 1e-6
            self.nb_cuts = 0
    
        def add_cut_constraint(self, ct):
            self.register_constraint(ct)
    
        @print_called("--> custom cut callback called: #{0}")
        def __call__(self):
            # fetch variable solution values at this point.
            sol = self.make_solution()
            # fetch those constraints which are not satisfied.
            unsats = self.get_cpx_unsatisfied_cts(self.cts, sol, self.eps)
            for ct, cut, sense, rhs in unsats:
                # Method add() here is CPLEX's CutCallback.add()
                self.add(cut, sense, rhs)
                self.nb_cuts += 1
                print('-- add new cut[{0}]: [{1!s}]'.format(self.nb_cuts, ct))
    
    
    def build_supply_model(fixed_costs, supply_costs, use_cuts=False, **kwargs):
        m = Model(name='suppy', **kwargs)
    
        nb_locations = len(fixed_costs)
        nb_clients = len(supply_costs)
        range_locations = range(nb_locations)
        range_clients = range(nb_clients)
    
        # --- Create variables. ---
        # - used[l]      If location l is used.
        # - supply[l][c] Amount shipped from location j to client c. This is a real
        #                number in [0,1] and specifies the percentage of c's
        #                demand that is served from location l.
        used = m.binary_var_list(range_locations, name='used')
        supply = m.continuous_var_matrix(range_clients, range_locations, lb=0, ub=1, name='supply')
        m.used = used
        m.supply = supply
        # --- add constraints ---
        # The supply for each client must sum to 1, i.e., the demand of each
        # client must be met.
        m.add_constraints(m.sum(supply[c, l] for l in range_locations) == 1 for c in range_clients)
        # Capacity constraint for each location. We just require that a single
        # location cannot serve all clients, that is, the capacity of each
        # location is nbClients-1. This makes the model a little harder to
        # solve and allows us to separate more cuts.
        m.add_constraints(
            m.sum(supply[c, l] for c in range_clients) <= (nb_clients - 1) * used[l] for l in range_locations)
    
        # Tweak some CPLEX parameters so that CPLEX has a harder time to
        # solve the model and our cut separators can actually kick in.
        # params = m.parameters
        # params.threads = 1
        # # params.mip.strategy.heuristicfreq = -1
        # params.mip.cuts.mircut = -1
        # params.mip.cuts.implied = -1
        # params.mip.cuts.gomory = -1
        # params.mip.cuts.flowcovers = -1
        # params.mip.cuts.pathcut = -1
        # params.mip.cuts.liftproj = -1
        # params.mip.cuts.zerohalfcut = -1
        # params.mip.cuts.cliques = -1
        # params.mip.cuts.covers = -1
    
        # --- set objective ---
        # objective is to minimize total cost, i.e. sum of location fixed cost and supply costs
        total_fixed_cost = m.dot(used, fixed_costs)
        m.add_kpi(total_fixed_cost, 'Total fixed cost')
        total_supply_cost = m.sum(supply[c, l] * supply_costs[c][l] for c in range_clients for l in range_locations)
        m.add_kpi(total_supply_cost, 'Total supply cost')
        m.minimize(total_fixed_cost + total_supply_cost)
    
        if use_cuts:
            # register a cut constraint callback
            # this links the model to the callback
            cut_cb = m.register_callback(MyCutCallback)
    
            # store cut constraints inside the callback, as DOcplex objects
            # here we add the folwing cuts:
            #   supply[c,l] <= use[l] for c in clients, l in locations.
            # These constraints are implied by the capacity constraints, but might be violated in LP solutions.
            for l in range_locations:
                location_used = used[l]
                for c in range_clients:
                    cut_cb.add_cut_constraint(supply[c, l] <= location_used)
            print('* add cut constraints callback with {0} cuts'.format(len(cut_cb.cts)))
    
        m.cut_callback = cut_cb
        return m
    
    
    def print_solution(m, tol=1e-6):
        used = m.used
        supply = m.supply
        n_locations = len(used)
        n_clients = (int)(len(supply) / n_locations)
        for l in range(n_locations):
            if used[l].solution_value >= 1 - tol:
                print('Facility %d is used, it serves clients %s' %
                      (l, ', '.join((str(c) for c in range(n_clients) if supply[c, l].solution_value >= 1 - tol))))
    
    
    # default data
    DEFAULT_FIXED_COSTS = [480, 200, 320, 340, 300]
    DEFAULT_SUPPLY_COSTS = [[24, 74, 31, 51, 84],
                            [57, 54, 86, 61, 68],
                            [57, 67, 29, 91, 71],
                            [54, 54, 65, 82, 94],
                            [98, 81, 16, 61, 27],
                            [13, 92, 34, 94, 87],
                            [54, 72, 41, 12, 78],
                            [54, 64, 65, 89, 89]]
    
    
    def build_test_supply_model(use_cuts, **kwargs):
        return build_supply_model(DEFAULT_FIXED_COSTS, DEFAULT_SUPPLY_COSTS, use_cuts=use_cuts, **kwargs)
    
    if __name__ == "__main__":
        # parse args
        args = sys.argv
        use_cuts = True
        for arg in args[1:]:
            if arg == '-cuts':
                use_cuts = False
            elif arg == '-nocuts':
                use_cuts = False
            else:
                print('Unknown argument %s' % arg)
        random = False
        if random:
            import numpy as np
            # trigger this to investigate higher volumes
            nl = 20
            nc = 100
            fixed = np.random.randint(100, high=500, size=nl)
            supply = np.random.randint(1, high=100, size=(nc, nl))
        else:
            fixed = DEFAULT_FIXED_COSTS
            supply = DEFAULT_SUPPLY_COSTS
    
        m = build_supply_model(fixed, supply, use_cuts=use_cuts)
        m.parameters.preprocessing.presolve = 0
        m.print_information()
    
        s = m.solve(log_output=False)
        assert s
        m.report()
        print_solution(m)
        # expected value is 843, regardless of cuts
        if not random:
            assert abs(m.objective_value - 843) <= 1e-4


    ------------------------------
    Daniel Junglas
    ------------------------------



  • 7.  RE: Docplex Python "Losing Constraint"

    Posted Mon May 25, 2020 08:24 AM

    Thank you very much. This will give a clear idea of how to start. 

    One difference that I noticed is that you don't add any new variable when you add the cuts. In my case, every time, I'll add a cut a new binary related to the specific cut? Do you know if callback can handle that? Adding a new constraint and new variables?

    Thank you



    ------------------------------
    Fernando Dias
    ------------------------------



  • 8.  RE: Docplex Python "Losing Constraint"

    Posted Mon May 25, 2020 09:15 AM
    Sorry, I missed the detail about the new variables. This is actually not possible with CPLEX. From a callback you can only add new constraints. During a solve, variables can neither be added nor removed. In some cases it is possible to create the required variables a-priori, protect them from being removed in presolve and then do something with them in a callback. But that smells a lot like a hack.

    ------------------------------
    Daniel Junglas
    ------------------------------



  • 9.  RE: Docplex Python "Losing Constraint"

    Posted Fri June 05, 2020 01:11 PM

    Thank you very much.

    I will start off a new thread given that the initial problem is solved a few weeks ago and now I have new questions related to callbacks.



    ------------------------------
    Fernando Dias
    ------------------------------