Skip to content

Commit

Permalink
⚡ Add linear constraints and variables in one transaction
Browse files Browse the repository at this point in the history
The `cplex` package's Linear Constraints and Variable interfaces allow for batched transactions. I think an appropriate design is to generate all the necessary data
and add these objects as one call to the `solver_model`. I've also removed unnecessary transactions such as resetting variable bounds immediately after adding that variable with an obsolete bound.
  • Loading branch information
ruaridhw committed Apr 28, 2020
1 parent 25567b9 commit 3e5ac43
Showing 1 changed file with 151 additions and 31 deletions.
182 changes: 151 additions & 31 deletions pyomo/solvers/plugins/solvers/cplex_direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,77 @@ def _is_numeric(x):
return True


class _VariableData(object):
def __init__(self, solver_model):
self._solver_model = solver_model
self.lb = []
self.ub = []
self.types = []
self.names = []

def add(self, lb, ub, type_, name):
self.lb.append(lb)
self.ub.append(ub)
self.types.append(type_)
self.names.append(name)

def __enter__(self):
return self

def __exit__(self, *excinfo):
self._solver_model.variables.add(
lb=self.lb, ub=self.ub, types=self.types, names=self.names
)


class _LinearConstraintData(object):
def __init__(self, solver_model):
self._solver_model = solver_model
self.lin_expr = []
self.senses = []
self.rhs = []
self.range_values = []
self.names = []

def add(self, cplex_expr, sense, rhs, range_values, name):
self.lin_expr.append([cplex_expr.variables, cplex_expr.coefficients])
self.senses.append(sense)
self.rhs.append(rhs)
self.range_values.append(range_values)
self.names.append(name)

def __enter__(self):
return self

def __exit__(self, *excinfo):
self._solver_model.linear_constraints.add(
lin_expr=self.lin_expr,
senses=self.senses,
rhs=self.rhs,
range_values=self.range_values,
names=self.names,
)


class nullcontext(object):
"""Context manager that does no additional processing.
Used as a stand-in for a normal context manager, when a particular
block of code is only sometimes used with a normal context manager:
cm = optional_cm if condition else nullcontext()
with cm:
# Perform operation, using optional_cm if condition is True
"""

def __init__(self, enter_result=None):
self.enter_result = enter_result

def __enter__(self):
return self.enter_result

def __exit__(self, *excinfo):
pass


@SolverFactory.register('cplex_direct', doc='Direct python interface to CPLEX')
class CPLEXDirect(DirectSolver):

Expand Down Expand Up @@ -248,7 +319,7 @@ def _get_expr_from_pyomo_expr(self, expr, max_degree=2):

return cplex_expr, referenced_vars

def _add_var(self, var):
def _add_var(self, var, cplex_var_data=None):
varname = self._symbol_map.getSymbol(var, self._labeler)
vtype = self._cplex_vtype_from_var(var)
if var.has_lb():
Expand All @@ -260,7 +331,14 @@ def _add_var(self, var):
else:
ub = self._cplex.infinity

self._solver_model.variables.add(lb=[lb], ub=[ub], types=[vtype], names=[varname])

ctx = (
_VariableData(self._solver_model)
if cplex_var_data is None
else nullcontext(cplex_var_data)
)
with ctx as cplex_var_data:
cplex_var_data.add(lb=lb, ub=ub, type_=vtype, name=varname)

self._pyomo_var_to_solver_var_map[var] = varname
self._solver_var_to_pyomo_var_map[varname] = var
Expand Down Expand Up @@ -303,7 +381,49 @@ def _set_instance(self, model, kwds={}):
"by overwriting its bounds in the CPLEX instance."
% (var.name, self._pyomo_model.name,))

def _add_constraint(self, con):
def _add_block(self, block):
with _VariableData(self._solver_model) as cplex_var_data:
for var in block.component_data_objects(
ctype=pyomo.core.base.var.Var, descend_into=True, active=True, sort=True
):
self._add_var(var, cplex_var_data)

with _LinearConstraintData(self._solver_model) as cplex_lin_con_data:
for sub_block in block.block_data_objects(descend_into=True, active=True):
for con in sub_block.component_data_objects(
ctype=pyomo.core.base.constraint.Constraint,
descend_into=False,
active=True,
sort=True,
):
if not con.has_lb() and not con.has_ub():
assert not con.equality
continue # non-binding, so skip

self._add_constraint(con, cplex_lin_con_data)

for con in sub_block.component_data_objects(
ctype=pyomo.core.base.sos.SOSConstraint,
descend_into=False,
active=True,
sort=True,
):
self._add_sos_constraint(con)

obj_counter = 0
for obj in sub_block.component_data_objects(
ctype=pyomo.core.base.objective.Objective,
descend_into=False,
active=True,
):
obj_counter += 1
if obj_counter > 1:
raise ValueError(
"Solver interface does not support multiple objectives."
)
self._set_objective(obj)

def _add_constraint(self, con, cplex_lin_con_data=None):
if not con.active:
return None

Expand All @@ -314,12 +434,12 @@ def _add_constraint(self, con):

if con._linear_canonical_form:
cplex_expr, referenced_vars = self._get_expr_from_pyomo_repn(
con.canonical_form(),
self._max_constraint_degree)
con.canonical_form(), self._max_constraint_degree
)
else:
cplex_expr, referenced_vars = self._get_expr_from_pyomo_expr(
con.body,
self._max_constraint_degree)
con.body, self._max_constraint_degree
)

if con.has_lb():
if not is_fixed(con.lower):
Expand All @@ -330,39 +450,39 @@ def _add_constraint(self, con):
raise ValueError("Upper bound of constraint {0} "
"is not constant.".format(con))

range_ = 0.0
if con.equality:
my_sense = 'E'
my_rhs = [value(con.lower) - cplex_expr.offset]
my_range = []
sense = "E"
rhs = value(con.lower) - cplex_expr.offset
elif con.has_lb() and con.has_ub():
my_sense = 'R'
sense = "R"
lb = value(con.lower)
ub = value(con.upper)
my_rhs = [ub - cplex_expr.offset]
my_range = [lb - ub]
rhs = ub - cplex_expr.offset
range_ = lb - ub
self._range_constraints.add(con)
elif con.has_lb():
my_sense = 'G'
my_rhs = [value(con.lower) - cplex_expr.offset]
my_range = []
sense = "G"
rhs = value(con.lower) - cplex_expr.offset
elif con.has_ub():
my_sense = 'L'
my_rhs = [value(con.upper) - cplex_expr.offset]
my_range = []
sense = "L"
rhs = value(con.upper) - cplex_expr.offset
else:
raise ValueError("Constraint does not have a lower "
"or an upper bound: {0} \n".format(con))
raise ValueError(
"Constraint does not have a lower "
"or an upper bound: {0} \n".format(con)
)

if len(cplex_expr.q_coefficients) == 0:
self._solver_model.linear_constraints.add(
lin_expr=[[cplex_expr.variables,
cplex_expr.coefficients]],
senses=my_sense,
rhs=my_rhs,
range_values=my_range,
names=[conname])
ctx = (
_LinearConstraintData(self._solver_model)
if cplex_lin_con_data is None
else nullcontext(cplex_lin_con_data)
)
with ctx as cplex_lin_con_data:
cplex_lin_con_data.add(cplex_expr, sense, rhs, range_, conname)
else:
if my_sense == 'R':
if sense == 'R':
raise ValueError("The CPLEXDirect interface does not "
"support quadratic range constraints: "
"{0}".format(con))
Expand All @@ -372,8 +492,8 @@ def _add_constraint(self, con):
quad_expr=[cplex_expr.q_variables1,
cplex_expr.q_variables2,
cplex_expr.q_coefficients],
sense=my_sense,
rhs=my_rhs[0],
sense=sense,
rhs=rhs,
name=conname)

for var in referenced_vars:
Expand Down

0 comments on commit 3e5ac43

Please sign in to comment.