coin-or / python-mip

Python-MIP: collection of Python tools for the modeling and solution of Mixed-Integer Linear programs
Eclipse Public License 2.0
516 stars 90 forks source link

Simple boolean problem has no solution when it should #207

Closed jessehersch closed 2 years ago

jessehersch commented 2 years ago

Describe the bug I have an optimization problem with boolean variables that appears to produce no solution when it should. It's not overconstrained. If I run the problem with PuLP, it finds a solution.

To Reproduce Here's a unittest that runs the same problem through pulp and mip. The mip test fails.

Maybe I am not setting up the constraints right for mip? In particular I am thinking there could be something wrong with the constrain_equal_sums since it's setting two sums equal to each other?

In any case the PuLP version works though.

from unittest import TestCase

import mip
import pulp

class IpModelTestCase(TestCase):
    def setUp(self):
        self.count = 20 # how many boolean variables we will have

        self.constrain_sums = [[0, 2, 4, 6, 8, 10, 12, 14, 16, 18],
                               [1, 3, 5, 7, 9, 11, 13, 15, 17, 19],
                               [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 0],
                               [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 17, 19, 2],
                               [5, 7, 9, 11, 13, 15, 17, 19, 3],
                               [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 19, 4],
                               [7, 9, 11, 13, 15, 17, 19, 5],
                               [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 6],
                               [9, 11, 13, 15, 17, 19, 7],
                               [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 8],
                               [11, 13, 15, 17, 19, 9],
                               [12, 13, 14, 15, 16, 17, 18, 19, 10],
                               [13, 15, 17, 19, 11],
                               [14, 15, 16, 17, 18, 19, 12],
                               [15, 17, 19, 13],
                               [16, 17, 18, 19, 14],
                               [17, 19, 15],
                               [18, 19, 16],
                               [19, 17]]
        self.constrain_equal_sums = [([1], [0]),
                                     ([3], [2]),
                                     ([5], [4]),
                                     ([7], [6]),
                                     ([9], [8]),
                                     ([11], [10]),
                                     ([13], [12]),
                                     ([15], [14]),
                                     ([17], [16]),
                                     ([19], [18])]

    def test_mip(self):
        model = mip.Model('mip')
        rewards = [3] * self.count
        variables = [model.add_var(var_type=mip.BINARY) for _ in range(self.count)]

        # set the objective function
        mip.maximize(mip.xsum(rewards[i] * variables[i] for i in range(self.count)))

        # constraints
        for indexes in self.constrain_sums:
            model.add_constr(mip.xsum(variables[i] for i in indexes) <= 1)

        for indexes_1, indexes_2 in self.constrain_equal_sums:
            model.add_constr(mip.xsum(variables[i] for i in indexes_1) == mip.xsum(variables[i] for i in indexes_2))

        # solve it
        model.optimize()
        selected = [i for i, var in enumerate(variables) if var.x > 0.99]

        # this fails because selected is empty
        self.assertEqual(2, len(selected))

    def test_pulp(self):
        model = pulp.LpProblem('pulp', pulp.LpMaximize)
        rewards = [3] * self.count

        variables = pulp.LpVariable.dicts('option', indexs=[str(i) for i in range(self.count)], cat=pulp.LpBinary)

        # set the objective function
        model += pulp.lpSum([rewards[i] * variables[str(i)] for i in range(self.count)])

        # constraints
        for indexes in self.constrain_sums:
            model += pulp.lpSum(variables[str(i)] for i in indexes) <= 1

        for indexes_1, indexes_2 in self.constrain_equal_sums:
            model += pulp.lpSum(variables[str(i)] for i in indexes_1) == pulp.lpSum(variables[str(i)] for i in indexes_2)

        # solve it
        model.solve(pulp.PULP_CBC_CMD())
        selected = [int(i) for i, var in variables.items() if var.varValue > 0.99]

        # this works because selected = [0, 1]
        self.assertEqual(2, len(selected))
        self.assertEqual([0, 1], selected)

Expected behavior I expect the boolean problem to have a solution

Versions

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
Codename:       focal

$ python --version
Python 3.6.9

$ pip freeze | grep mip
mip==1.13.0

Additional context can't think of anything. The repro above should be straightforward.

jurasofish commented 2 years ago

perhaps you should do something with the line mip.maximize(mip.xsum(rewards[i] * variables[i] for i in range(self.count)))

jurasofish commented 2 years ago

all you have done is create a linear expression, you havent done anything with it

jessehersch commented 2 years ago

ok so I am not setting the objective function correctly? I guess it should be this:

model.objective = mip.maximize(mip.xsum(rewards[i] * variables[i] for i in range(self.count)))
jessehersch commented 2 years ago

indeed that's it. i'm an idiot :)

this should be closed as "user error" or similar. don't think I can close it

tuliotoffolo commented 2 years ago

No problem. Issue closed. :)