Python: Optimization with Exponential Objective Function
Question:
I am working on assignment problems. So far, I have been using Gurobi
via gurobipy. In my current problem, I have an objective function with an exponential term. In gurobipy this would be
m.setObjective(ratings.prod(assign) ** 0.5, sense=GRB.MAXIMIZE)
but **
(or pow()
) is not supported in Gurobi. What are other options (with other tools than Gurobi) or workarounds?
Below is a minimal working example:
import pandas as pd
import gurobipy as gp
from gurobipy import GRB
import random
def solve():
m = gp.Model()
assign = m.addVars(permutations, vtype=gp.GRB.BINARY, name="assign")
m.addConstrs((assign.sum(i, "*") == 1 for i in individuals))
capacity = {
'P1':40, 'P2':30, 'P3':30
}
m.addConstrs(
(assign.sum("*", p) <= capacity[p] for p in groups),
name="limit"
)
# objective function
m.setObjective(ratings.prod(assign) ** 0.5, sense=GRB.MAXIMIZE)
m.optimize()
return m, assign
def get_results(assign):
# create df with placeholder
results = pd.DataFrame(-1, index=individuals, columns=groups)
# fill in the results
for (i, j), x_ij in assign.items():
results.loc[i, j] = int(x_ij.X)
return results
# minimal example
outcomes = []
for i in range(0,100):
x = random.uniform(0, 1)
outcomes.append(x)
data = {'P1': outcomes,
'P2': random.sample(outcomes, len(outcomes)),
'P3': random.sample(outcomes, len(outcomes))}
df = pd.DataFrame(data)
individuals = list(df.index)
groups = ['P1', 'P2', 'P3']
# prepare
permutations, ratings = gp.multidict({
(i, j): df.loc[i, j]
for i in individuals
for j in groups
})
# run
m, assign = solve()
results = get_results(assign)
printresults)
Answers:
As mentioned in comment, just take the sqrt after the fact… Unless I’m missing something fundamental in the gurobi syntax. Instead of using this:
m.setObjective(ratings.prod(assign) ** 0.5, sense=GRB.MAXIMIZE)
use this:
m.setObjective(ratings.prod(assign), sense=GRB.MAXIMIZE)
Then solve the model with this objective, which will get the same values for assign
and then after the solve, get the value of the objective out of the model (not sure how to do this in gurobi syntax, so this is psuedocode:
obj_value = m.get_objective_value_somehow_in_gurobi_syntax ??
and then just take the sqrt of that…
real_obj_value = math.sqrt(obj_value)
the values of the variables that maximize obj_value
and real_obj_value
are the same.
Edit: working with terms with sqrt
New plan… per comments you actually intend to use sqrt of individual terms in the sum-product, so the above strategy will not work.
However, because your decision variables are binary, you can implement a work-around….
Make a separate dictionary of "corrected values" that is just the sqrt of the individual values. (I’m assuming you still want the original values for use in some constraints, etc. so retain and use those as well) They are both indexed identically, so use the originals in the constraints and the corrected in the objective.
So in psuedocode something like:
corr_values = {k:v**0.5 for k, v in values_dict}
then just use the corrected values in your objective function, which is mathematically fine because you are only multiplying by 1 or 0:
1 * v**0.5 == (1 * v)**0.5
and you will get the outcome you desire and the low values will be weighted appropriately
I am working on assignment problems. So far, I have been using Gurobi
via gurobipy. In my current problem, I have an objective function with an exponential term. In gurobipy this would be
m.setObjective(ratings.prod(assign) ** 0.5, sense=GRB.MAXIMIZE)
but **
(or pow()
) is not supported in Gurobi. What are other options (with other tools than Gurobi) or workarounds?
Below is a minimal working example:
import pandas as pd
import gurobipy as gp
from gurobipy import GRB
import random
def solve():
m = gp.Model()
assign = m.addVars(permutations, vtype=gp.GRB.BINARY, name="assign")
m.addConstrs((assign.sum(i, "*") == 1 for i in individuals))
capacity = {
'P1':40, 'P2':30, 'P3':30
}
m.addConstrs(
(assign.sum("*", p) <= capacity[p] for p in groups),
name="limit"
)
# objective function
m.setObjective(ratings.prod(assign) ** 0.5, sense=GRB.MAXIMIZE)
m.optimize()
return m, assign
def get_results(assign):
# create df with placeholder
results = pd.DataFrame(-1, index=individuals, columns=groups)
# fill in the results
for (i, j), x_ij in assign.items():
results.loc[i, j] = int(x_ij.X)
return results
# minimal example
outcomes = []
for i in range(0,100):
x = random.uniform(0, 1)
outcomes.append(x)
data = {'P1': outcomes,
'P2': random.sample(outcomes, len(outcomes)),
'P3': random.sample(outcomes, len(outcomes))}
df = pd.DataFrame(data)
individuals = list(df.index)
groups = ['P1', 'P2', 'P3']
# prepare
permutations, ratings = gp.multidict({
(i, j): df.loc[i, j]
for i in individuals
for j in groups
})
# run
m, assign = solve()
results = get_results(assign)
printresults)
As mentioned in comment, just take the sqrt after the fact… Unless I’m missing something fundamental in the gurobi syntax. Instead of using this:
m.setObjective(ratings.prod(assign) ** 0.5, sense=GRB.MAXIMIZE)
use this:
m.setObjective(ratings.prod(assign), sense=GRB.MAXIMIZE)
Then solve the model with this objective, which will get the same values for assign
and then after the solve, get the value of the objective out of the model (not sure how to do this in gurobi syntax, so this is psuedocode:
obj_value = m.get_objective_value_somehow_in_gurobi_syntax ??
and then just take the sqrt of that…
real_obj_value = math.sqrt(obj_value)
the values of the variables that maximize obj_value
and real_obj_value
are the same.
Edit: working with terms with sqrt
New plan… per comments you actually intend to use sqrt of individual terms in the sum-product, so the above strategy will not work.
However, because your decision variables are binary, you can implement a work-around….
Make a separate dictionary of "corrected values" that is just the sqrt of the individual values. (I’m assuming you still want the original values for use in some constraints, etc. so retain and use those as well) They are both indexed identically, so use the originals in the constraints and the corrected in the objective.
So in psuedocode something like:
corr_values = {k:v**0.5 for k, v in values_dict}
then just use the corrected values in your objective function, which is mathematically fine because you are only multiplying by 1 or 0:
1 * v**0.5 == (1 * v)**0.5
and you will get the outcome you desire and the low values will be weighted appropriately