Skip to content

Commit

Permalink
Trac #20414: Add copy/__copy__ methods to CVXOPT, PPL, InteractiveLP …
Browse files Browse the repository at this point in the history
…backends, and __deepcopy__ to MixedIntegerLinearProgram and backends

The COIN, CPLEX, GLPK, and Gurobi backends supply a `copy` method.
But it's not documented as a backend interface method in
`GenericBackend`, and the CVXOPT, PPL, InteractiveLP backends do not
implement it.
It is used by `MixedIntegerLinearProgram.__copy__`.

The backend method should actually probably be called `__copy__` as well
-- see https://docs.python.org/2/library/copy.html

The branch on the ticket does this.

We also add `__deepcopy__` methods to `MixedIntegerLinearProgram` and
the backends.
This fixes #15159, though the semantics of `copy` and `deepcopy` of a
`MixedIntegerLinearProgram` remains questionable due to its interaction
with `MIPVariable`; see #15159 and #19523.

See also #20323 (in which copying a backend could be the basis for more
diverse tests).

URL: http://trac.sagemath.org/20414
Reported by: mkoeppe
Ticket author(s): Matthias Koeppe
Reviewer(s): Dima Pasechnik
  • Loading branch information
Release Manager authored and vbraun committed Apr 19, 2016
2 parents 16a89e2 + 3117b01 commit 8124729
Show file tree
Hide file tree
Showing 14 changed files with 266 additions and 9 deletions.
2 changes: 1 addition & 1 deletion src/sage/numerical/backends/coin_backend.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ cdef class CoinBackend(GenericBackend):
cdef list col_names, row_names
cdef str prob_name

cpdef CoinBackend copy(self)
cpdef __copy__(self)
cpdef get_basis_status(self)
cpdef int set_basis_status(self, list cstat, list rstat) except -1
cpdef get_binva_row(self, int i)
Expand Down
5 changes: 4 additions & 1 deletion src/sage/numerical/backends/coin_backend.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,9 @@ cdef class CoinBackend(GenericBackend):

self.si.addCol (1, c_indices, c_values, 0, self.si.getInfinity(), 0)

self.col_names.append("")


cpdef int solve(self) except -1:
r"""
Solves the problem.
Expand Down Expand Up @@ -1206,7 +1209,7 @@ cdef class CoinBackend(GenericBackend):
else:
return ""

cpdef CoinBackend copy(self):
cpdef __copy__(self):
"""
Returns a copy of self.
Expand Down
2 changes: 1 addition & 1 deletion src/sage/numerical/backends/cplex_backend.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ cdef class CPLEXBackend(GenericBackend):
cdef c_cpxlp * lp
cdef current_sol
cdef str _logfilename
cpdef CPLEXBackend copy(self)
cpdef __copy__(self)

cdef extern from "cplex.h":

Expand Down
2 changes: 1 addition & 1 deletion src/sage/numerical/backends/cplex_backend.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1449,7 +1449,7 @@ cdef class CPLEXBackend(GenericBackend):
status = CPXwriteprob(self.env, self.lp, filename, ext)
check(status)

cpdef CPLEXBackend copy(self):
cpdef __copy__(self):
r"""
Returns a copy of self.
Expand Down
38 changes: 38 additions & 0 deletions src/sage/numerical/backends/cvxopt_backend.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ AUTHORS:

from sage.numerical.mip import MIPSolverException
from cvxopt import solvers
from copy import copy

cdef class CVXOPTBackend(GenericBackend):
"""
Expand Down Expand Up @@ -95,6 +96,43 @@ cdef class CVXOPTBackend(GenericBackend):
else:
self.set_sense(-1)

cpdef __copy__(self):
# Added a second inequality to this doctest,
# because otherwise CVXOPT complains: ValueError: Rank(A) < p or Rank([G; A]) < n
"""
Returns a copy of self.
EXAMPLE::
sage: from sage.numerical.backends.generic_backend import get_solver
sage: p = MixedIntegerLinearProgram(solver = "CVXOPT")
sage: b = p.new_variable()
sage: p.add_constraint(b[1] + b[2] <= 6)
sage: p.add_constraint(b[2] <= 5)
sage: p.set_objective(b[1] + b[2])
sage: cp = copy(p.get_backend())
sage: cp.solve()
0
sage: cp.get_objective_value()
6.0
"""
cp = CVXOPTBackend()
cp.objective_function = self.objective_function[:]
cp.G_matrix = [row[:] for row in self.G_matrix]
cp.prob_name = self.prob_name
cp.obj_constant_term = self.obj_constant_term
cp.is_maximize = self.is_maximize

cp.row_lower_bound = self.row_lower_bound[:]
cp.row_upper_bound = self.row_upper_bound[:]
cp.col_lower_bound = self.col_lower_bound[:]
cp.col_upper_bound = self.col_upper_bound[:]

cp.row_name_var = self.row_name_var[:]
cp.col_name_var = self.col_name_var[:]

cp.param = copy(self.param)
return cp

cpdef int add_variable(self, lower_bound=0.0, upper_bound=None, binary=False, continuous=True, integer=False, obj=None, name=None) except -1:
"""
Expand Down
2 changes: 2 additions & 0 deletions src/sage/numerical/backends/generic_backend.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ cdef class GenericBackend (SageObject):
cpdef solver_parameter(self, name, value=*)
cpdef zero(self)
cpdef base_ring(self)
cpdef __copy__(self)
cpdef copy(self)
cpdef bint is_variable_basic(self, int index)
cpdef bint is_variable_nonbasic_at_lower_bound(self, int index)
cpdef bint is_slack_variable_basic(self, int index)
Expand Down
127 changes: 127 additions & 0 deletions src/sage/numerical/backends/generic_backend.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ AUTHORS:
# http://www.gnu.org/licenses/
#*****************************************************************************

from copy import copy

cdef class GenericBackend:

cpdef base_ring(self):
Expand Down Expand Up @@ -828,6 +830,61 @@ cdef class GenericBackend:
"""
raise NotImplementedError()

cpdef copy(self):
"""
Returns a copy of self.
EXAMPLE::
sage: from sage.numerical.backends.generic_backend import get_solver
sage: p = MixedIntegerLinearProgram(solver = "Nonexistent_LP_solver") # optional - Nonexistent_LP_solver
sage: b = p.new_variable() # optional - Nonexistent_LP_solver
sage: p.add_constraint(b[1] + b[2] <= 6) # optional - Nonexistent_LP_solver
sage: p.set_objective(b[1] + b[2]) # optional - Nonexistent_LP_solver
sage: copy(p).solve() # optional - Nonexistent_LP_solver
6.0
"""
return self.__copy__()

# Override this method in backends.
cpdef __copy__(self):
"""
Returns a copy of self.
EXAMPLE::
sage: from sage.numerical.backends.generic_backend import get_solver
sage: p = MixedIntegerLinearProgram(solver = "Nonexistent_LP_solver") # optional - Nonexistent_LP_solver
sage: b = p.new_variable() # optional - Nonexistent_LP_solver
sage: p.add_constraint(b[1] + b[2] <= 6) # optional - Nonexistent_LP_solver
sage: p.set_objective(b[1] + b[2]) # optional - Nonexistent_LP_solver
sage: cp = copy(p.get_backend()) # optional - Nonexistent_LP_solver
sage: cp.solve() # optional - Nonexistent_LP_solver
0
sage: cp.get_objective_value() # optional - Nonexistent_LP_solver
6.0
"""
raise NotImplementedError()

def __deepcopy__(self, memo={}):
"""
Return a deep copy of ``self``.
EXAMPLE::
sage: from sage.numerical.backends.generic_backend import get_solver
sage: p = MixedIntegerLinearProgram(solver = "Nonexistent_LP_solver") # optional - Nonexistent_LP_solver
sage: b = p.new_variable() # optional - Nonexistent_LP_solver
sage: p.add_constraint(b[1] + b[2] <= 6) # optional - Nonexistent_LP_solver
sage: p.set_objective(b[1] + b[2]) # optional - Nonexistent_LP_solver
sage: cp = deepcopy(p.get_backend()) # optional - Nonexistent_LP_solver
sage: cp.solve() # optional - Nonexistent_LP_solver
0
sage: cp.get_objective_value() # optional - Nonexistent_LP_solver
6.0
"""
return self.__copy__()

cpdef row(self, int i):
"""
Return a row
Expand Down Expand Up @@ -1024,6 +1081,76 @@ cdef class GenericBackend:
"""
raise NotImplementedError()

def _do_test_problem_data(self, tester, cp):
"""
TESTS:
Test, with an actual working backend, that comparing a problem with itself works::
sage: from sage.numerical.backends.generic_backend import get_solver
sage: p = get_solver(solver='GLPK')
sage: tester = p._tester()
sage: p._do_test_problem_data(tester, p)
"""
tester.assertEqual(type(self), type(cp),
"Classes do not match")
def assert_equal_problem_data(method):
tester.assertEqual(getattr(self, method)(), getattr(cp, method)(),
"{} does not match".format(method))
for method in ("ncols", "nrows", "objective_constant_term", "problem_name", "is_maximization"):
assert_equal_problem_data(method)
def assert_equal_col_data(method):
for i in range(self.ncols()):
tester.assertEqual(getattr(self, method)(i), getattr(cp, method)(i),
"{}({}) does not match".format(method, i))
for method in ("objective_coefficient", "is_variable_binary", "is_variable_binary", "is_variable_integer",
"is_variable_continuous", "col_bounds", "col_name"):
# don't test variable_lower_bound, variable_upper_bound because we already test col_bounds.
# TODO: Add a test elsewhere to ensure that variable_lower_bound, variable_upper_bound
# are consistent with col_bounds.
assert_equal_col_data(method)
def assert_equal_row_data(method):
for i in range(self.nrows()):
tester.assertEqual(getattr(self, method)(i), getattr(cp, method)(i),
"{}({}) does not match".format(method, i))
for method in ("row_bounds", "row", "row_name"):
assert_equal_row_data(method)

def _test_copy(self, **options):
"""
Test whether the backend can be copied
and at least the problem data of the copy is equal to that of the original.
Does not test whether solutions or solver parameters are copied.
"""
tester = self._tester(**options)
cp = copy(self)
self._do_test_problem_data(tester, cp)

def _test_copy_does_not_share_data(self, **options):
"""
Test whether copy makes an independent copy of the backend.
"""
tester = self._tester(**options)
cp = copy(self)
cpcp = copy(cp)
del cp
self._do_test_problem_data(tester, cpcp)

# TODO: We should have a more systematic way of generating MIPs for testing.
@classmethod
def _test_copy_some_mips(cls, tester=None, **options):
p = cls() # fresh instance of the backend
if tester is None:
tester = p._tester(**options)
# From doctest of GenericBackend.solve:
p.add_linear_constraints(5, 0, None)
# p.add_col(range(5), range(5)) -- bad test because COIN sparsifies the 0s away on copy
p.add_col(range(5), range(1, 6))
# From doctest of GenericBackend.problem_name:
p.problem_name("There once was a french fry")
p._test_copy(**options)
p._test_copy_does_not_share_data(**options)

cpdef variable_upper_bound(self, int index, value = False):
"""
Return or define the upper bound on a variable
Expand Down
2 changes: 1 addition & 1 deletion src/sage/numerical/backends/glpk_backend.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ cdef class GLPKBackend(GenericBackend):
cdef glp_smcp * smcp
cdef int simplex_or_intopt
cdef search_tree_data_t search_tree_data
cpdef GLPKBackend copy(self)
cpdef __copy__(self)
cpdef int print_ranges(self, char * filename = *) except -1
cpdef double get_row_dual(self, int variable)
cpdef double get_col_dual(self, int variable)
Expand Down
2 changes: 1 addition & 1 deletion src/sage/numerical/backends/glpk_backend.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1611,7 +1611,7 @@ cdef class GLPKBackend(GenericBackend):
"""
glp_write_mps(self.lp, modern, NULL, filename)

cpdef GLPKBackend copy(self):
cpdef __copy__(self):
"""
Returns a copy of self.
Expand Down
2 changes: 1 addition & 1 deletion src/sage/numerical/backends/gurobi_backend.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ cdef class GurobiBackend(GenericBackend):
cdef GRBenv * env
cdef GRBenv * env_master
cdef GRBmodel * model
cpdef GurobiBackend copy(self)
cpdef __copy__(self)

cdef int num_vars

Expand Down
2 changes: 1 addition & 1 deletion src/sage/numerical/backends/gurobi_backend.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1171,7 +1171,7 @@ cdef class GurobiBackend(GenericBackend):
else:
raise RuntimeError("This should not happen.")

cpdef GurobiBackend copy(self):
cpdef __copy__(self):
"""
Returns a copy of self.
Expand Down
26 changes: 25 additions & 1 deletion src/sage/numerical/backends/interactivelp_backend.pyx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
r"""
COIN Backend
InteractiveLP Backend
AUTHORS:
Expand All @@ -22,6 +22,7 @@ AUTHORS:
from sage.numerical.mip import MIPSolverException
from sage.numerical.interactive_simplex_method import InteractiveLPProblem, default_variable_name
from sage.modules.all import vector
from copy import copy

cdef class InteractiveLPBackend:
"""
Expand Down Expand Up @@ -87,6 +88,29 @@ cdef class InteractiveLPBackend:

self.row_names = []

cpdef __copy__(self):
"""
Returns a copy of self.
EXAMPLE::
sage: from sage.numerical.backends.generic_backend import get_solver
sage: p = MixedIntegerLinearProgram(solver = "InteractiveLP")
sage: b = p.new_variable()
sage: p.add_constraint(b[1] + b[2] <= 6)
sage: p.set_objective(b[1] + b[2])
sage: cp = copy(p.get_backend())
sage: cp.solve()
0
sage: cp.get_objective_value()
6
"""
cp = InteractiveLPBackend(base_ring=self.base_ring())
cp.lp = self.lp # it's considered immutable; so no need to copy.
cp.row_names = copy(self.row_names)
cp.prob_name = self.prob_name
return cp

cpdef base_ring(self):
"""
Return the base ring.
Expand Down
34 changes: 34 additions & 0 deletions src/sage/numerical/backends/ppl_backend.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ from sage.numerical.mip import MIPSolverException
from sage.libs.ppl import MIP_Problem, Variable, Variables_Set, Linear_Expression, Constraint, Generator
from sage.rings.integer cimport Integer
from sage.rings.rational cimport Rational
from copy import copy

cdef class PPLBackend(GenericBackend):

Expand Down Expand Up @@ -98,6 +99,39 @@ cdef class PPLBackend(GenericBackend):
cpdef zero(self):
return self.base_ring()(0)

cpdef __copy__(self):
"""
Returns a copy of self.
EXAMPLE::
sage: from sage.numerical.backends.generic_backend import get_solver
sage: p = MixedIntegerLinearProgram(solver = "PPL")
sage: b = p.new_variable()
sage: p.add_constraint(b[1] + b[2] <= 6)
sage: p.set_objective(b[1] + b[2])
sage: cp = copy(p.get_backend())
sage: cp.solve()
0
sage: cp.get_objective_value()
6
"""
cp = PPLBackend()
cp.Matrix = [row[:] for row in self.Matrix]
cp.row_lower_bound = self.row_lower_bound[:]
cp.row_upper_bound = self.row_upper_bound[:]
cp.col_lower_bound = self.col_lower_bound[:]
cp.col_upper_bound = self.col_upper_bound[:]
cp.objective_function = self.objective_function[:]
cp.row_name_var = self.row_name_var[:]
cp.col_name_var = self.col_name_var[:]
cp.name = self.name
cp.obj_constant_term = self.obj_constant_term
cp.obj_denominator = self.obj_denominator
cp.integer_variables = copy(self.integer_variables)
cp.is_maximize = self.is_maximize
return cp

def init_mip(self):
"""
Converting the matrix form of the MIP Problem to PPL MIP_Problem.
Expand Down
Loading

0 comments on commit 8124729

Please sign in to comment.