Skip to content

Commit 3e5ac43

Browse files
author
Ruaridh Williamson
committed
⚡ Add linear constraints and variables in one transaction
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.
1 parent 25567b9 commit 3e5ac43

File tree

1 file changed

+151
-31
lines changed

1 file changed

+151
-31
lines changed

pyomo/solvers/plugins/solvers/cplex_direct.py

Lines changed: 151 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,77 @@ def _is_numeric(x):
6262
return True
6363

6464

65+
class _VariableData(object):
66+
def __init__(self, solver_model):
67+
self._solver_model = solver_model
68+
self.lb = []
69+
self.ub = []
70+
self.types = []
71+
self.names = []
72+
73+
def add(self, lb, ub, type_, name):
74+
self.lb.append(lb)
75+
self.ub.append(ub)
76+
self.types.append(type_)
77+
self.names.append(name)
78+
79+
def __enter__(self):
80+
return self
81+
82+
def __exit__(self, *excinfo):
83+
self._solver_model.variables.add(
84+
lb=self.lb, ub=self.ub, types=self.types, names=self.names
85+
)
86+
87+
88+
class _LinearConstraintData(object):
89+
def __init__(self, solver_model):
90+
self._solver_model = solver_model
91+
self.lin_expr = []
92+
self.senses = []
93+
self.rhs = []
94+
self.range_values = []
95+
self.names = []
96+
97+
def add(self, cplex_expr, sense, rhs, range_values, name):
98+
self.lin_expr.append([cplex_expr.variables, cplex_expr.coefficients])
99+
self.senses.append(sense)
100+
self.rhs.append(rhs)
101+
self.range_values.append(range_values)
102+
self.names.append(name)
103+
104+
def __enter__(self):
105+
return self
106+
107+
def __exit__(self, *excinfo):
108+
self._solver_model.linear_constraints.add(
109+
lin_expr=self.lin_expr,
110+
senses=self.senses,
111+
rhs=self.rhs,
112+
range_values=self.range_values,
113+
names=self.names,
114+
)
115+
116+
117+
class nullcontext(object):
118+
"""Context manager that does no additional processing.
119+
Used as a stand-in for a normal context manager, when a particular
120+
block of code is only sometimes used with a normal context manager:
121+
cm = optional_cm if condition else nullcontext()
122+
with cm:
123+
# Perform operation, using optional_cm if condition is True
124+
"""
125+
126+
def __init__(self, enter_result=None):
127+
self.enter_result = enter_result
128+
129+
def __enter__(self):
130+
return self.enter_result
131+
132+
def __exit__(self, *excinfo):
133+
pass
134+
135+
65136
@SolverFactory.register('cplex_direct', doc='Direct python interface to CPLEX')
66137
class CPLEXDirect(DirectSolver):
67138

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

249320
return cplex_expr, referenced_vars
250321

251-
def _add_var(self, var):
322+
def _add_var(self, var, cplex_var_data=None):
252323
varname = self._symbol_map.getSymbol(var, self._labeler)
253324
vtype = self._cplex_vtype_from_var(var)
254325
if var.has_lb():
@@ -260,7 +331,14 @@ def _add_var(self, var):
260331
else:
261332
ub = self._cplex.infinity
262333

263-
self._solver_model.variables.add(lb=[lb], ub=[ub], types=[vtype], names=[varname])
334+
335+
ctx = (
336+
_VariableData(self._solver_model)
337+
if cplex_var_data is None
338+
else nullcontext(cplex_var_data)
339+
)
340+
with ctx as cplex_var_data:
341+
cplex_var_data.add(lb=lb, ub=ub, type_=vtype, name=varname)
264342

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

306-
def _add_constraint(self, con):
384+
def _add_block(self, block):
385+
with _VariableData(self._solver_model) as cplex_var_data:
386+
for var in block.component_data_objects(
387+
ctype=pyomo.core.base.var.Var, descend_into=True, active=True, sort=True
388+
):
389+
self._add_var(var, cplex_var_data)
390+
391+
with _LinearConstraintData(self._solver_model) as cplex_lin_con_data:
392+
for sub_block in block.block_data_objects(descend_into=True, active=True):
393+
for con in sub_block.component_data_objects(
394+
ctype=pyomo.core.base.constraint.Constraint,
395+
descend_into=False,
396+
active=True,
397+
sort=True,
398+
):
399+
if not con.has_lb() and not con.has_ub():
400+
assert not con.equality
401+
continue # non-binding, so skip
402+
403+
self._add_constraint(con, cplex_lin_con_data)
404+
405+
for con in sub_block.component_data_objects(
406+
ctype=pyomo.core.base.sos.SOSConstraint,
407+
descend_into=False,
408+
active=True,
409+
sort=True,
410+
):
411+
self._add_sos_constraint(con)
412+
413+
obj_counter = 0
414+
for obj in sub_block.component_data_objects(
415+
ctype=pyomo.core.base.objective.Objective,
416+
descend_into=False,
417+
active=True,
418+
):
419+
obj_counter += 1
420+
if obj_counter > 1:
421+
raise ValueError(
422+
"Solver interface does not support multiple objectives."
423+
)
424+
self._set_objective(obj)
425+
426+
def _add_constraint(self, con, cplex_lin_con_data=None):
307427
if not con.active:
308428
return None
309429

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

315435
if con._linear_canonical_form:
316436
cplex_expr, referenced_vars = self._get_expr_from_pyomo_repn(
317-
con.canonical_form(),
318-
self._max_constraint_degree)
437+
con.canonical_form(), self._max_constraint_degree
438+
)
319439
else:
320440
cplex_expr, referenced_vars = self._get_expr_from_pyomo_expr(
321-
con.body,
322-
self._max_constraint_degree)
441+
con.body, self._max_constraint_degree
442+
)
323443

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

453+
range_ = 0.0
333454
if con.equality:
334-
my_sense = 'E'
335-
my_rhs = [value(con.lower) - cplex_expr.offset]
336-
my_range = []
455+
sense = "E"
456+
rhs = value(con.lower) - cplex_expr.offset
337457
elif con.has_lb() and con.has_ub():
338-
my_sense = 'R'
458+
sense = "R"
339459
lb = value(con.lower)
340460
ub = value(con.upper)
341-
my_rhs = [ub - cplex_expr.offset]
342-
my_range = [lb - ub]
461+
rhs = ub - cplex_expr.offset
462+
range_ = lb - ub
343463
self._range_constraints.add(con)
344464
elif con.has_lb():
345-
my_sense = 'G'
346-
my_rhs = [value(con.lower) - cplex_expr.offset]
347-
my_range = []
465+
sense = "G"
466+
rhs = value(con.lower) - cplex_expr.offset
348467
elif con.has_ub():
349-
my_sense = 'L'
350-
my_rhs = [value(con.upper) - cplex_expr.offset]
351-
my_range = []
468+
sense = "L"
469+
rhs = value(con.upper) - cplex_expr.offset
352470
else:
353-
raise ValueError("Constraint does not have a lower "
354-
"or an upper bound: {0} \n".format(con))
471+
raise ValueError(
472+
"Constraint does not have a lower "
473+
"or an upper bound: {0} \n".format(con)
474+
)
355475

356476
if len(cplex_expr.q_coefficients) == 0:
357-
self._solver_model.linear_constraints.add(
358-
lin_expr=[[cplex_expr.variables,
359-
cplex_expr.coefficients]],
360-
senses=my_sense,
361-
rhs=my_rhs,
362-
range_values=my_range,
363-
names=[conname])
477+
ctx = (
478+
_LinearConstraintData(self._solver_model)
479+
if cplex_lin_con_data is None
480+
else nullcontext(cplex_lin_con_data)
481+
)
482+
with ctx as cplex_lin_con_data:
483+
cplex_lin_con_data.add(cplex_expr, sense, rhs, range_, conname)
364484
else:
365-
if my_sense == 'R':
485+
if sense == 'R':
366486
raise ValueError("The CPLEXDirect interface does not "
367487
"support quadratic range constraints: "
368488
"{0}".format(con))
@@ -372,8 +492,8 @@ def _add_constraint(self, con):
372492
quad_expr=[cplex_expr.q_variables1,
373493
cplex_expr.q_variables2,
374494
cplex_expr.q_coefficients],
375-
sense=my_sense,
376-
rhs=my_rhs[0],
495+
sense=sense,
496+
rhs=rhs,
377497
name=conname)
378498

379499
for var in referenced_vars:

0 commit comments

Comments
 (0)