• Gurobi Staff

Hi Bhartendu,

It is certainly is possible.  Did you perhaps want to provide a concrete example with code or a math formulation?  Or we can just make one up to demonstrate.

- Riley

Thanks Riley for the prompt response.

model = gp.Model("")ind = pd.Series(range(500))bin_varb = gppd.add_vars(model, ind, vtype="B") int_varb = gppd.add_vars(model, ind, vtype="I")# I want to express the below relationship# if bin_varb[i] == 0 => int_varb[i] <= 0 for i in 0 .. 499# I am doing like belowbig_M = 1000constr = gppd.add_constrs(    model, int_varb - (big_M * bin_varb), GRB.LESS_EQUAL, 0)
# with gurobipy, I would do like below, but would have to loop through all indicesfor i in range(500):    model.addGenConstrIndicator(bin_varb[i], 0, int_varb[i] <= 0)

I am looking to express the above via indicator constraints with gurobipy pandas.

Thanks

• Gurobi Staff

Hi Bhartendu,

There is no built-in method in gurobipy-pandas which can add multiple indicator constraints at once.  What this means is that the approach will be more "pandas with gurobipy" rather than "gurobipy-pandas".

So, we have to add each indicator constraint one by one, but there are many ways we could do this.  Purely for academic reasons I set about trying as many as I could think of and collecting timing results using Python's timeit module.

This is the setup code:

import timeitimport pandas as pdsetup = """import gurobipy as gpfrom gurobipy import GRBimport gurobipy_pandas as gppdimport pandas as pdmodel = gp.Model("")ind = pd.Series(range(500))bin_varb = gppd.add_vars(model, ind, vtype="B") int_varb = gppd.add_vars(model, ind, vtype="I")# combining series into a single dataframedf = pd.concat([bin_varb, int_varb], axis=1)"""def min_runtime(stmt):    return min(timeit.repeat(        stmt,        setup=setup,        repeat=100,        number=1,    ))

The min_runtime function is defined so that we run each possible approach 100 times and take the minimum (minimum runtimes are important when comparing approaches to remove variability by other processes on the machine).

These are all the different ways we can add indicator constraints, with the final result being a pandas.Series of the constraints.  The code is expressed in strings as this is what timeit requires.

zip_addConstr_code = """pd.Series(    [model.addConstr((b == 0) >> (i <= 0))    for b,i in zip(bin_varb, int_varb)],    index = ind,)"""zip_addGenConstrIndicator_code = """pd.Series(    [model.addGenConstrIndicator(b, 0, i, GRB.LESS_EQUAL, 0)    for b,i in zip(bin_varb, int_varb)],    index = ind,)"""df_apply_expr_apply_addConstr = """df.apply(lambda r: (r[0] == 0) >> (r[1] <= 0), axis=1).apply(model.addConstr)"""df_apply_addConstr = """df.apply(lambda r: model.addConstr((r[0] == 0) >> (r[1] <= 0)), axis=1)"""df_apply_addGenConstrIndicator = """df.apply(lambda r: model.addGenConstrIndicator(r[0], 0, r[1], GRB.LESS_EQUAL, 0), axis=1)"""df_itertuples_addConstr = """pd.Series(    (model.addConstr((r._1 == 0) >> (r._2 <= 0))    for r in df.itertuples()),    index=df.index,)"""df_itertuples_addGenConstrIndicator = """pd.Series(    [model.addGenConstrIndicator(r._1, 0, r._2, GRB.LESS_EQUAL, 0)    for r in df.itertuples()],    index=df.index,)"""df_iterrows_addConstr = """pd.Series(    (model.addConstr((r[0] == 0) >> (r[1] <= 0))    for _, r in df.iterrows()),    index=df.index,)"""df_iterrows_addGenConstrIndicator = """pd.Series(    (model.addGenConstrIndicator(r[0], 0, r[1], GRB.LESS_EQUAL, 0)    for _, r in df.iterrows()),    index=df.index,)"""

Then a pandas.Series can be created by running each of these approaches through the min_runtime function, and then dividing by the time of the fastest approach:

results = pd.Series(    {        string_name: min_runtime(eval(string_name))        for string_name in (            "zip_addGenConstrIndicator_code",            "zip_addConstr_code",            "df_apply_expr_apply_addConstr",            "df_apply_addConstr",            "df_apply_addGenConstrIndicator",            "df_itertuples_addConstr",            "df_itertuples_addGenConstrIndicator",            "df_iterrows_addConstr",            "df_iterrows_addGenConstrIndicator",        )    })results_normalized = (results/results.min()).sort_values()

Then, results_normalized looks like this, and represent the performance of each approach as a multiple of the fastest approach found (zip_addGenConstrIndicator_code):

zip_addGenConstrIndicator_code         1.000000df_itertuples_addGenConstrIndicator    1.062170zip_addConstr_code                     1.663099df_itertuples_addConstr                1.742310df_apply_expr_apply_addConstr          2.001572df_apply_addGenConstrIndicator         2.001766df_apply_addConstr                     2.852161df_iterrows_addGenConstrIndicator      5.110845df_iterrows_addConstr                  6.337511dtype: float64

- Using addGenConstrIndicator is faster than addConstr.  This is not surprising since addConstr handles multiple types of constraints and this incurs an overhead to facilitate this.

- Using itertuples is much faster than iterrows.  This is also not surprising since itertuples is well known to be much faster than iterrows (in general, not specifically to this code).

- The fastest approach is to just pair the variables up with the zip function, but the approach with itertuples is not too far behind, so "the best" approach might be a matter of taste in this instance.

- Riley

• Gurobi Staff

Part 2: A more complicated example

Say that we have binary variables $$x_{i,k}$$ and $$z_{k,n}$$, which share a common index $$k$$, a vector of constants $$C_k$$, and we want the following indicator constraints:

$x_{i,k} = 0 \rightarrow \sum_{n}z_{k,n} \leq C_k, \quad\quad \forall i,k$

Setup:

import gurobipy as gpfrom gurobipy import GRBimport gurobipy_pandas as gppdimport numpy as npimport pandas as pdi = (1,2)k = (1,2,3)n = ("a","b")model = gp.Model()x = gppd.add_vars(model, pd.MultiIndex.from_product((i,k), names=("i", "k")), vtype=GRB.BINARY, name="x")z = gppd.add_vars(model, pd.MultiIndex.from_product((k,n), names=("k", "n")), vtype=GRB.BINARY, name="z")C = pd.Series(    np.random.randint(1,10, size=len(k)),    index=pd.Index(k, name="k"),)

Note that

z.groupby("k").sum()

will give us linear expressions which sum up z variables for each k indice:

k1    z[1,a] + z[1,b]2    z[2,a] + z[2,b]3    z[3,a] + z[3,b]Name: z, dtype: object

We can then join this with x and C:

df = x.to_frame().join(z.groupby("k").sum()).join(C.to_frame("C"))

to give

                       x                z  Ci k                                         1 1  <gurobi.Var x[1,1]>  z[1,a] + z[1,b]  3  2  <gurobi.Var x[1,2]>  z[2,a] + z[2,b]  2  3  <gurobi.Var x[1,3]>  z[3,a] + z[3,b]  42 1  <gurobi.Var x[2,1]>  z[1,a] + z[1,b]  3  2  <gurobi.Var x[2,2]>  z[2,a] + z[2,b]  2  3  <gurobi.Var x[2,3]>  z[3,a] + z[3,b]  4

We can then add the indicator constraints using the itertuples & addGenConstrIndicator approach:

pd.Series(    [model.addGenConstrIndicator(r.x, 0, r.z, GRB.LESS_EQUAL, r.C)    for r in df.itertuples()],    index=df.index,)

- Riley

WOW ! Thanks Riley for such a detailed explanation. Everything is crystal clear now.
I will definitely use the above patterns in my live projects.

Thanks again,
Bhartendu