Skip to content

Commit

Permalink
Clone with lp format (#40)
Browse files Browse the repository at this point in the history
* refactor: make a tempfile util function

All pickling now uses the TemporaryFilename context manager which
creates a temporary file that gets deleted afterwards. The file must be
opened every time it is used.

* feat: clone models via LP format if available

Fall back on old clone method if an intermediary LP format is not
possible (i.e. an interface does not support it)

* chore: update model before getstate and to_lp

* fix: flake8

* feat: add lp switch to clone

* test: test clone with and without lp
  • Loading branch information
KristianJensen authored Nov 15, 2016
1 parent bc7a4fe commit 0e446f3
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 88 deletions.
43 changes: 18 additions & 25 deletions optlang/cplex_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,13 @@

log = logging.getLogger(__name__)

import tempfile
import sympy
from sympy.core.add import _unevaluated_Add
from sympy.core.mul import _unevaluated_Mul
from sympy.core.singleton import S
import cplex
from optlang import interface
from optlang.util import inheritdocstring
from optlang.util import inheritdocstring, TemporaryFilename
from optlang.expression_parsing import parse_optimization_expression

Zero = S.Zero
Expand Down Expand Up @@ -590,36 +589,34 @@ def __init__(self, problem=None, *args, **kwargs):
raise TypeError("Provided problem is not a valid CPLEX model.")
self.configuration = Configuration(problem=self, verbosity=0)

@classmethod
def from_lp(cls, lp_form):
problem = cplex.Cplex()
with TemporaryFilename(suffix=".lp", content=lp_form) as tmp_file_name:
problem.read(tmp_file_name)
model = cls(problem=problem)
return model

def __getstate__(self):
self.update()
tmp_file = tempfile.NamedTemporaryFile(suffix=".sav", delete=False)
tmp_file_name = tmp_file.name
tmp_file.close()
try:
with TemporaryFilename(suffix=".sav") as tmp_file_name:
self.problem.write(tmp_file_name)
with open(tmp_file_name, "rb") as tmp_file:
cplex_binary = tmp_file.read()
finally:
os.remove(tmp_file_name)
repr_dict = {'cplex_binary': cplex_binary, 'status': self.status, 'config': self.configuration}
return repr_dict

def __setstate__(self, repr_dict):
tmp_file = tempfile.NamedTemporaryFile(suffix=".sav", delete=False, mode='wb')
tmp_file_name = tmp_file.name
tmp_file.close()
try:
with TemporaryFilename(suffix=".sav") as tmp_file_name:
with open(tmp_file_name, "wb") as tmp_file:
tmp_file.write(repr_dict['cplex_binary'])
tmp_file.write(repr_dict["cplex_binary"])
problem = cplex.Cplex()
# turn off logging completely, get's configured later
problem.set_error_stream(None)
problem.set_warning_stream(None)
problem.set_log_stream(None)
problem.set_results_stream(None)
problem.read(tmp_file_name)
finally:
os.remove(tmp_file_name)
if repr_dict['status'] == 'optimal':
problem.solve() # since the start is an optimal solution, nothing will happen here
self.__init__(problem=problem)
Expand Down Expand Up @@ -686,17 +683,13 @@ def shadow_prices(self):
return collections.OrderedDict(
zip((constraint.name for constraint in self.constraints), self.problem.solution.get_dual_values()))

def __str__(self):
tmp_file = tempfile.NamedTemporaryFile(suffix=".lp", mode='r', delete=False)
tmp_file_name = tmp_file.name
tmp_file.close()
try:
self.problem.write(tmp_file.name)
def to_lp(self):
self.update()
with TemporaryFilename(suffix=".lp") as tmp_file_name:
self.problem.write(tmp_file_name)
with open(tmp_file_name) as tmp_file:
cplex_form = tmp_file.read()
finally:
os.remove(tmp_file_name)
return cplex_form
lp_form = tmp_file.read()
return lp_form

def _optimize(self):
self.problem.solve()
Expand Down
51 changes: 20 additions & 31 deletions optlang/glpk_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,14 @@

import collections
import logging
import tempfile

import os
import six
import sympy
from sympy.core.add import _unevaluated_Add
from sympy.core.mul import _unevaluated_Mul

from optlang.util import inheritdocstring
from optlang.util import inheritdocstring, TemporaryFilename
from optlang.expression_parsing import parse_optimization_expression
from optlang import interface

Expand All @@ -52,7 +51,7 @@
glp_set_mat_row, glp_set_col_bnds, glp_set_row_bnds, GLP_FR, GLP_UP, GLP_LO, GLP_FX, GLP_DB, glp_del_rows, \
glp_get_mat_row, glp_get_row_ub, glp_get_row_type, glp_get_row_lb, glp_get_row_name, glp_get_obj_coef, \
glp_get_obj_dir, glp_scale_prob, GLP_SF_AUTO, glp_get_num_int, glp_get_num_bin, glp_mip_col_val, \
glp_mip_obj_val, glp_mip_status, GLP_ETMLIM, glp_adv_basis
glp_mip_obj_val, glp_mip_status, GLP_ETMLIM, glp_adv_basis, glp_read_lp



Expand Down Expand Up @@ -505,23 +504,23 @@ def __init__(self, problem=None, *args, **kwargs):
direction={GLP_MIN: 'min', GLP_MAX: 'max'}[glp_get_obj_dir(self.problem)])
glp_scale_prob(self.problem, GLP_SF_AUTO)

@classmethod
def from_lp(cls, lp_form):
problem = glp_create_prob()
with TemporaryFilename(suffix=".lp", content=lp_form) as tmp_file_name:
glp_read_lp(problem, None, tmp_file_name)
model = cls(problem=problem)
return model

def __getstate__(self):
self.update()
glpk_repr = self._glpk_representation()
repr_dict = {'glpk_repr': glpk_repr, 'glpk_status': self.status, 'config': self.configuration}
return repr_dict

def __setstate__(self, repr_dict):
tmp_file = tempfile.NamedTemporaryFile(suffix=".glpk", delete=False, mode='wb')
tmp_file_name = tmp_file.name
tmp_file.close()
try:
with open(tmp_file_name, "w") as tmp_file:
tmp_file.write(repr_dict['glpk_repr'])
with TemporaryFilename(suffix=".glpk", content=repr_dict["glpk_repr"]) as tmp_file_name:
problem = glp_create_prob()
glp_read_prob(problem, 0, tmp_file_name)
finally:
os.remove(tmp_file_name)
self.__init__(problem=problem)
self.configuration = Configuration.clone(repr_dict['config'], problem=self)
if repr_dict['glpk_status'] == 'optimal':
Expand Down Expand Up @@ -595,28 +594,20 @@ def shadow_prices(self):
shadow_prices[constraint.name] = value
return shadow_prices

def __str__(self):
tmp_file = tempfile.NamedTemporaryFile(suffix=".lp", mode='r', delete=False)
tmp_file_name = tmp_file.name
tmp_file.close()
try:
glp_write_lp(self.problem, None, tmp_file.name)
def to_lp(self):
self.update()
with TemporaryFilename(suffix=".lp") as tmp_file_name:
glp_write_lp(self.problem, None, tmp_file_name)
with open(tmp_file_name) as tmp_file:
cplex_form = tmp_file.read()
finally:
os.remove(tmp_file_name)
return cplex_form
lp_form = tmp_file.read()
return lp_form

def _glpk_representation(self):
tmp_file = tempfile.NamedTemporaryFile(suffix=".glpk", delete=False)
tmp_file_name = tmp_file.name
tmp_file.close()
try:
glp_write_prob(self.problem, 0, tmp_file.name)
self.update()
with TemporaryFilename(suffix=".glpk") as tmp_file_name:
glp_write_prob(self.problem, 0, tmp_file_name)
with open(tmp_file_name) as tmp_file:
glpk_form = tmp_file.read()
finally:
os.remove(tmp_file_name)
return glpk_form

def _run_glp_simplex(self):
Expand Down Expand Up @@ -809,8 +800,6 @@ def _remove_constraints(self, constraints):

print(model)

from swiglpk import glp_read_lp

problem = glp_create_prob()
glp_read_lp(problem, None, "../tests/data/model.lp")

Expand Down
50 changes: 20 additions & 30 deletions optlang/gurobi_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@

import os
import six
import tempfile
from optlang import interface
from optlang.util import inheritdocstring
from optlang.util import inheritdocstring, TemporaryFilename
from optlang.expression_parsing import parse_optimization_expression
import sympy
from sympy.core.add import _unevaluated_Add
Expand Down Expand Up @@ -455,29 +454,25 @@ def __init__(self, problem=None, *args, **kwargs):

self.configuration = Configuration(problem=self, verbosity=0)

@classmethod
def from_lp(cls, lp_form):
with TemporaryFilename(suffix=".lp", content=lp_form) as tmp_file_name:
problem = gurobipy.read(tmp_file_name)
model = cls(problem=problem)
return model

def __getstate__(self):
tmp_file = tempfile.NamedTemporaryFile(suffix=".lp", delete=False)
tmp_file_name = tmp_file.name
tmp_file.close()
try:
self.problem.write(tmp_file.name)
self.update()
with TemporaryFilename(suffix=".lp") as tmp_file_name:
self.problem.write(tmp_file_name)
with open(tmp_file_name) as tmp_file:
lp = tmp_file.read()
finally:
os.remove(tmp_file_name)
repr_dict = {'lp': lp, 'status': self.status, 'config': self.configuration}
lp_form = tmp_file.read()
repr_dict = {'lp': lp_form, 'status': self.status, 'config': self.configuration}
return repr_dict

def __setstate__(self, repr_dict):
tmp_file = tempfile.NamedTemporaryFile(suffix=".lp", delete=False, mode='wb')
tmp_file_name = tmp_file.name
tmp_file.close()
try:
with open(tmp_file_name, "w") as tmp_file:
tmp_file.write(repr_dict['lp'])
problem = gurobipy.read(tmp_file.name)
finally:
os.remove(tmp_file_name)
with TemporaryFilename(suffix=".lp", content=repr_dict["lp"]) as tmp_file_name:
problem = gurobipy.read(tmp_file_name)
# if repr_dict['status'] == 'optimal': # TODO: uncomment this
# # turn off logging completely, get's configured later
# problem.set_error_stream(None)
Expand All @@ -488,18 +483,13 @@ def __setstate__(self, repr_dict):
self.__init__(problem=problem)
self.configuration = Configuration.clone(repr_dict['config'], problem=self) # TODO: make configuration work

def __str__(self):
def to_lp(self):
self.problem.update()
tmp_file = tempfile.NamedTemporaryFile(suffix=".lp", mode='r', delete=False)
tmp_file_name = tmp_file.name
tmp_file.close()
try:
self.problem.write(tmp_file.name)
with TemporaryFilename(suffix=".lp") as tmp_file_name:
self.problem.write(tmp_file_name)
with open(tmp_file_name) as tmp_file:
cplex_form = tmp_file.read()
finally:
os.remove(tmp_file_name)
return cplex_form
lp_form = tmp_file.read()
return lp_form

@property
def objective(self):
Expand Down
9 changes: 8 additions & 1 deletion optlang/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1040,7 +1040,7 @@ class Model(object):
"""

@classmethod
def clone(cls, model):
def clone(cls, model, use_lp=True):
"""
Make a copy of a model. The model being copied can be of the same type or belong to
a different solver interface. This is the preferred way of copying models.
Expand All @@ -1051,6 +1051,11 @@ def clone(cls, model):
"""
model.update()
interface = sys.modules[cls.__module__]
if use_lp and hasattr(cls, "from_lp") and hasattr(model, "to_lp"):
new_model = cls.from_lp(model.to_lp())
new_model.configuration = interface.Configuration.clone(model.configuration, problem=new_model)
return new_model

new_model = cls()
for variable in model.variables:
new_variable = interface.Variable.clone(variable)
Expand Down Expand Up @@ -1188,6 +1193,8 @@ def shadow_prices(self):
return collections.OrderedDict([(constraint.name, constraint.dual) for constraint in self.constraints])

def __str__(self):
if hasattr(self, "to_lp"):
return self.to_lp()
self.update()
return '\n'.join((
str(self.objective),
Expand Down
10 changes: 9 additions & 1 deletion optlang/tests/abstract_test_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,14 +518,22 @@ def test_change_objective_can_handle_removed_vars(self):
self.model.update()
self.model.objective = self.interface.Objective(self.model.variables[1] * 2)

def test_clone_model(self):
def test_clone_model_with_lp(self):
self.assertEquals(self.model.configuration.verbosity, 0)
self.model.configuration.verbosity = 3
cloned_model = self.interface.Model.clone(self.model)
self.assertEquals(cloned_model.configuration.verbosity, 3)
self.assertEquals(len(cloned_model.variables), len(self.model.variables))
self.assertEquals(len(cloned_model.constraints), len(self.model.constraints))

def test_clone_model_without_lp(self):
self.assertEquals(self.model.configuration.verbosity, 0)
self.model.configuration.verbosity = 3
cloned_model = self.interface.Model.clone(self.model, use_lp=False)
self.assertEquals(cloned_model.configuration.verbosity, 3)
self.assertEquals(len(cloned_model.variables), len(self.model.variables))
self.assertEquals(len(cloned_model.constraints), len(self.model.constraints))


@six.add_metaclass(abc.ABCMeta)
class AbstractConfigurationTestCase(unittest.TestCase):
Expand Down
33 changes: 33 additions & 0 deletions optlang/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,39 @@ def parse_expr(expr, local_dict=None):
raise NotImplementedError(expr["type"] + " is not implemented")


class TemporaryFilename(object):
"""
Use context manager to create a temporary file that can be opened and closed, and will be deleted in the end.
Parameters
----------
suffix : str
The file ending. Default is 'tmp'
content : str or None
If str, the content will be written to the file upon creation
Example
----------
>>> with TemporaryFilename() as tmp_file_name:
>>> with open(tmp_file_name, "w") as tmp_file:
>>> tmp_file.write(stuff)
>>> with open(tmp_file) as tmp_file:
>>> stuff = tmp_file.read()
"""
def __init__(self, suffix="tmp", content=None):
tmp_file = tempfile.NamedTemporaryFile(suffix=suffix, delete=False, mode="w")
if content is not None:
tmp_file.write(content)
self.name = tmp_file.name
tmp_file.close()

def __enter__(self):
return self.name

def __exit__(self, type, value, traceback):
os.remove(self.name)


if __name__ == '__main__':
from swiglpk import glp_create_prob, glp_read_lp, glp_get_num_rows

Expand Down

0 comments on commit 0e446f3

Please sign in to comment.