Skip to content

Commit

Permalink
gh-35103: SCIPBackend: Faster copy, remove_constraint methods
Browse files Browse the repository at this point in the history
    
<!-- ^^^^^
Please provide a concise, informative and self-explanatory title.
Don't put issue numbers in there, do this in the PR body below.
For example, instead of "Fixes #1234" use "Introduce new method to
calculate 1+1"
-->
### 📚 Description
Fixes #34890

Authors: @mkoeppe, @mantepse

<!-- Describe your changes here in detail -->
<!-- Why is this change required? What problem does it solve? -->
<!-- If it resolves an open issue, please link to the issue here. For
example "Closes #1337" -->

### 📝 Checklist

<!-- Put an `x` in all the boxes that apply. -->
<!-- If your change requires a documentation PR, please link it
appropriately -->
<!-- If you're unsure about any of these, don't hesitate to ask. We're
here to help! -->

- [ ] I have made sure that the title is self-explanatory and the
description concisely explains the PR.
- [ ] I have linked an issue or discussion.
- [ ] I have created tests covering the changes.
- [ ] I have updated the documentation accordingly.

### ⌛ Dependencies
<!-- List all open pull requests that this PR logically depends on -->
<!--
- #xyz: short description why this is a dependency
- #abc: ...
-->
    
URL: #35103
Reported by: Matthias Köppe
Reviewer(s): Martin Rubey, Matthias Köppe, Travis Scrimshaw
  • Loading branch information
Release Manager committed Feb 21, 2024
2 parents 30b3d78 + cc55c9e commit be3a116
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 27 deletions.
4 changes: 3 additions & 1 deletion src/sage/numerical/backends/generic_backend.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,9 @@ cdef class GenericBackend:
sage: p.add_linear_constraint([(0, 3), (1, 2)], None, 6)
sage: p.remove_constraints([0, 1])
"""
if isinstance(constraints, int): self.remove_constraint(constraints)
if isinstance(constraints, int):
self.remove_constraint(constraints)
return

cdef int last = self.nrows() + 1

Expand Down
141 changes: 115 additions & 26 deletions src/sage/numerical/backends/scip_backend.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ cdef class SCIPBackend(GenericBackend):
def get_constraints(self):
"""
Get all constraints of the problem.
EXAMPLES::
sage: from sage.numerical.backends.generic_backend import get_solver
sage: lp = get_solver(solver="SCIP")
sage: lp.add_variables(3)
2
sage: lp.add_linear_constraint(zip([0, 1, 2], [8, 6, 1]), None, 48)
sage: lp.add_linear_constraint(zip([0, 1, 2], [2, 1.5, 0.5]), None, 8)
sage: lp.get_constraints()
[c1, c2]
sage: lp.row(1) # indirect doctest
([0, 1, 2], [2.0, 1.5, 0.5])
"""
if self.constraints is None:
self.constraints = self.model.getConss()
Expand All @@ -71,10 +85,11 @@ cdef class SCIPBackend(GenericBackend):
Get the model as a pyscipopt Model.
EXAMPLES::
sage: from sage.numerical.backends.generic_backend import get_solver
sage: p = get_solver(solver = "SCIP")
sage: p._get_model()
<pyscipopt.scip.Model object at ...
sage: from sage.numerical.backends.generic_backend import get_solver
sage: p = get_solver(solver = "SCIP")
sage: p._get_model()
<pyscipopt.scip.Model object at ...
"""
return self.model

Expand Down Expand Up @@ -369,6 +384,42 @@ cdef class SCIPBackend(GenericBackend):
self.model.delCons(self.get_constraints()[i])
self.constraints = None

cpdef remove_constraints(self, constraints) noexcept:
r"""
Remove several constraints.
INPUT:
- ``constraints`` -- an iterable containing the indices of the rows to remove
EXAMPLES::
sage: from sage.numerical.backends.generic_backend import get_solver
sage: p = get_solver(solver='SCIP')
sage: p.add_variables(2)
1
sage: p.add_linear_constraint([(0, 2), (1, 3)], None, 6)
sage: p.add_linear_constraint([(0, 3), (1, 2)], None, 6)
sage: p.row(0)
([0, 1], [2.0, 3.0])
sage: p.remove_constraints([0, 1])
sage: p.nrows()
0
"""
if isinstance(constraints, int):
self.remove_constraint(constraints)
return

if self.model.getStatus() != 'unknown':
self.model.freeTransform()
self.constraints = None

all_constraints = self.get_constraints()
to_remove = [all_constraints[i] for i in constraints]
for constraint in to_remove:
self.model.delCons(constraint)
self.constraints = None

cpdef add_linear_constraint(self, coefficients, lower_bound, upper_bound, name=None) noexcept:
"""
Add a linear constraint.
Expand Down Expand Up @@ -528,7 +579,7 @@ cdef class SCIPBackend(GenericBackend):
INPUT:
- ``indices`` (list of integers) -- this list constains the
- ``indices`` (list of integers) -- this list contains the
indices of the constraints in which the variable's
coefficient is nonzero
Expand Down Expand Up @@ -692,6 +743,20 @@ cdef class SCIPBackend(GenericBackend):
EXAMPLES::
sage: # needs sage.graphs
sage: g = graphs.CubeGraph(9)
sage: p = MixedIntegerLinearProgram(solver="SCIP")
sage: p.solver_parameter("limits/gap", 100)
sage: b = p.new_variable(binary=True)
sage: p.set_objective(p.sum(b[v] for v in g))
sage: for v in g:
....: p.add_constraint(b[v]+p.sum(b[u] for u in g.neighbors(v)) <= 1)
sage: p.add_constraint(b[v] == 1) # Force an easy non-0 solution
sage: p.solve() # rel tol 100
1.0
sage: backend = p.get_backend()
sage: backend.best_known_objective_bound() # random
31.0
"""
return self.model.getPrimalbound()

Expand All @@ -713,12 +778,28 @@ cdef class SCIPBackend(GenericBackend):
EXAMPLES::
sage: # needs sage.graphs
sage: g = graphs.CubeGraph(9)
sage: p = MixedIntegerLinearProgram(solver="SCIP")
sage: p.solver_parameter("limits/gap", 100)
sage: b = p.new_variable(binary=True)
sage: p.set_objective(p.sum(b[v] for v in g))
sage: for v in g:
....: p.add_constraint(b[v]+p.sum(b[u] for u in g.neighbors(v)) <= 1)
sage: p.add_constraint(b[v] == 1) # Force an easy non-0 solution
sage: p.solve() # rel tol 100
1.0
sage: backend = p.get_backend()
sage: backend.get_relative_objective_gap() # random
46.99999999999999
TESTS:
Just make sure that the variable *has* been defined, and is not just
undefined::
sage: backend.get_relative_objective_gap() > 1 # needs sage.graphs
True
"""
return self.model.getGap()

Expand Down Expand Up @@ -811,7 +892,26 @@ cdef class SCIPBackend(GenericBackend):
sage: p.add_linear_constraints(2, 2, None)
sage: p.nrows()
2
TESTS::
After calling :meth:`remove_constraints` we know that
`self.constraints is None`. `SCIP` keeps track of the number
of constraints, so we can do the optimization::
sage: from sage.numerical.backends.generic_backend import get_solver
sage: p = get_solver(solver='SCIP')
sage: p.add_variables(2)
1
sage: p.add_linear_constraint([(0, 2), (1, 3)], None, 6)
sage: p.row(0)
([0, 1], [2.0, 3.0])
sage: p.remove_constraints([0])
sage: p.nrows()
0
"""
if self.constraints is None:
return self.model.getNConss()
return len(self.get_constraints())

cpdef col_name(self, int index) noexcept:
Expand Down Expand Up @@ -870,7 +970,6 @@ cdef class SCIPBackend(GenericBackend):
sage: p.set_variable_type(0,0)
sage: p.is_variable_binary(0)
True
"""
return self.variables[index].vtype() == 'BINARY'

Expand Down Expand Up @@ -1155,27 +1254,10 @@ cdef class SCIPBackend(GenericBackend):
6.0
"""
cdef SCIPBackend cp = type(self)(maximization=self.is_maximization())
cp.model = Model(sourceModel=self.model, origcopy=True)
cp.problem_name(self.problem_name())
for i, v in enumerate(self.variables):
vtype = v.vtype()
cp.add_variable(self.variable_lower_bound(i),
self.variable_upper_bound(i),
binary=vtype == 'BINARY',
continuous=vtype == 'CONTINUOUS',
integer=vtype == 'INTEGER',
obj=self.objective_coefficient(i),
name=self.col_name(i))
assert self.ncols() == cp.ncols()

for i in range(self.nrows()):
coefficients = zip(*self.row(i))
lower_bound, upper_bound = self.row_bounds(i)
name = self.row_name(i)
cp.add_linear_constraint(coefficients,
lower_bound,
upper_bound,
name=name)
assert self.nrows() == cp.nrows()
cp.obj_constant_term = self.obj_constant_term
cp.variables = cp.model.getVars()
return cp

cpdef solver_parameter(self, name, value=None) noexcept:
Expand All @@ -1189,6 +1271,13 @@ cdef class SCIPBackend(GenericBackend):
- ``value`` -- the parameter's value if it is to be defined,
or ``None`` (default) to obtain its current value.
EXAMPLES:
sage: from sage.numerical.backends.generic_backend import get_solver
sage: lp = get_solver(solver="SCIP")
sage: p.solver_parameter("limits/time", 1)
sage: p.solver_parameter("limits/time")
1.0
"""
if value is not None:
if name.lower() == 'timelimit':
Expand Down

0 comments on commit be3a116

Please sign in to comment.