Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add branching priorities to CPLEXSHELL #1300

Merged
merged 19 commits into from
Mar 17, 2020
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion pyomo/opt/base/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

__all__ = [ 'AbstractProblemWriter', 'WriterFactory', 'ProblemConfigFactory', 'BaseProblemConfig' ]
__all__ = [
"AbstractProblemWriter",
"WriterFactory",
"ProblemConfigFactory",
"BaseProblemConfig",
"BranchDirection",
]

from pyomo.common import Factory

Expand Down Expand Up @@ -44,3 +50,12 @@ def __enter__(self):
def __exit__(self, t, v, traceback):
pass


class BranchDirection(object):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open question: is this the best place for this enum-like object?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wondered the same thing, but I'm not sure of a better place for it. Gurobi has the same options (up, down, and default).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this was the only place general enough that I could think of as there's nothing else as suitable. We could always introduce a new module for this though?

""" Allowed values for MIP variable branching directions in the `direction` Suffix of a model. """

default = 0
down = -1
up = 1

ALL = {default, down, up}
124 changes: 121 additions & 3 deletions pyomo/solvers/plugins/solvers/CPLEX.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
from pyomo.opt.results import *
from pyomo.opt.solver import *
from pyomo.solvers.mockmip import MockMIP
from pyomo.core.base import Var, ComponentMap, Suffix, active_export_suffix_generator
from pyomo.core.kernel.block import IBlock
from pyomo.util.components import iter_component

logger = logging.getLogger('pyomo.solvers')

Expand Down Expand Up @@ -112,6 +114,28 @@ def __new__(cls, *args, **kwds):
return opt


class ORDFileSchema(object):
HEADER = "* ENCODING=ISO-8859-1\nNAME Priority Order\n"
FOOTER = "ENDATA\n"

@classmethod
def ROW(cls, name, priority, branch_direction=None):
return " %s %s %s\n" % (
cls._direction_to_str(branch_direction),
name,
priority,
)

@staticmethod
def _direction_to_str(branch_direction):
try:
return {BranchDirection.down: "DN", BranchDirection.up: "UP"}[
branch_direction
]
except KeyError:
return ""


@SolverFactory.register('_cplex_shell', doc='Shell interface to the CPLEX LP/MIP solver')
class CPLEXSHELL(ILMLicensedSystemCallSolver):
"""Shell interface to the CPLEX LP/MIP solver
Expand Down Expand Up @@ -162,9 +186,6 @@ def warm_start_capable(self):
# write a warm-start file in the CPLEX MST format.
#
def _warm_start(self, instance):

from pyomo.core.base import Var

# for each variable in the symbol_map, add a child to the
# variables element. Both continuous and discrete are accepted
# (and required, depending on other options), according to the
Expand Down Expand Up @@ -201,6 +222,72 @@ def _warm_start(self, instance):
mst_file.write("</variables>\n")
mst_file.write("</CPLEXSolution>\n")

# Expected names of `Suffix` components for branching priorities and directions respectively
SUFFIX_PRIORITY_NAME = "priority"
SUFFIX_DIRECTION_NAME = "direction"

def _write_priorities_file(self, instance):
""" Write a variable priorities file in the CPLEX ORD format. """
priorities, directions = self._get_suffixes(instance)
rows = self._convert_priorities_to_rows(instance, priorities, directions)
self._write_priority_rows(rows)

def _get_suffixes(self, instance):
if isinstance(instance, IBlock):
suffixes = (
(suf.name, suf)
for suf in pyomo.core.kernel.suffix.export_suffix_generator(
instance, datatype=Suffix.INT, active=True, descend_into=False
)
)
else:
suffixes = active_export_suffix_generator(instance, datatype=Suffix.INT)
suffixes = dict(suffixes)

if self.SUFFIX_PRIORITY_NAME not in suffixes:
raise ValueError(
"Cannot write branching priorities file as `model.%s` Suffix has not been declared."
% (self.SUFFIX_PRIORITY_NAME,)
)

return (
suffixes[self.SUFFIX_PRIORITY_NAME],
suffixes.get(self.SUFFIX_DIRECTION_NAME, ComponentMap()),
)

def _convert_priorities_to_rows(self, instance, priorities, directions):
if isinstance(instance, IBlock):
smap = getattr(instance, "._symbol_maps")[self._smap_id]
else:
smap = instance.solutions.symbol_map[self._smap_id]
byObject = smap.byObject

rows = []
for var, priority in priorities.items():
if priority is None or not var.active:
continue

if not (0 <= priority == int(priority)):
raise ValueError("`priority` must be a non-negative integer")

var_direction = directions.get(var, BranchDirection.default)

for child_var in iter_component(var):
if id(child_var) not in byObject:
continue

child_var_direction = directions.get(child_var, var_direction)

rows.append((byObject[id(child_var)], priority, child_var_direction))
return rows

def _write_priority_rows(self, rows):
with open(self._priorities_file_name, "w") as ord_file:
ord_file.write(ORDFileSchema.HEADER)
for var_name, priority, direction in rows:
ord_file.write(ORDFileSchema.ROW(var_name, priority, direction))
ord_file.write(ORDFileSchema.FOOTER)

# over-ride presolve to extract the warm-start keyword, if specified.
def _presolve(self, *args, **kwds):

Expand Down Expand Up @@ -234,6 +321,21 @@ def _presolve(self, *args, **kwds):
self._warm_start_file_name = pyutilib.services.TempfileManager.\
create_tempfile(suffix = '.cplex.mst')

self._priorities_solve = kwds.pop("priorities", False)
self._priorities_file_name = _validate_file_name(
self, kwds.pop("priorities_file", None), "branching priorities"
)
user_priorities = self._priorities_file_name is not None

if (
self._priorities_solve
and not isinstance(args[0], basestring)
and not user_priorities
):
self._priorities_file_name = pyutilib.services.TempfileManager.create_tempfile(
suffix=".cplex.ord"
)

# let the base class handle any remaining keywords/actions.
ILMLicensedSystemCallSolver._presolve(self, *args, **kwds)

Expand All @@ -259,6 +361,16 @@ def _presolve(self, *args, **kwds):
print("Warm start write time= %.2f seconds"
% (end_time-start_time))

if self._priorities_solve and (not user_priorities):
start_time = time.time()
self._write_priorities_file(args[0])
end_time = time.time()
if self._report_timing:
print(
"Branching priorities write time= %.2f seconds"
% (end_time - start_time)
)

def _default_executable(self):
executable = pyomo.common.Executable("cplex")
if not executable:
Expand Down Expand Up @@ -328,6 +440,9 @@ def create_command_line(self, executable, problem_files):
(self._warm_start_file_name is not None):
script += 'read %s\n' % (self._warm_start_file_name,)

if self._priorities_solve and self._priorities_file_name is not None:
script += "read %s\n" % (self._priorities_file_name,)

if 'relax_integrality' in self.options:
script += 'change problem lp\n'

Expand All @@ -351,6 +466,9 @@ def create_command_line(self, executable, problem_files):
print("Solver warm-start file="
+self._warm_start_file_name)

if self._priorities_solve and self._priorities_file_name is not None:
print("Solver priorities file=" + self._priorities_file_name)

#
# Define command line
#
Expand Down
Loading