Skip to content

Commit 16580f6

Browse files
fvz185Joao-DionisioCopilotmmghannam
authored
Extend Functionality to Use the Presolver Plugin and Add a Tutorial (#1076)
* Add SCIPvarIsActive function and corresponding test for variable activity * Add SCIPaggregateVars function and aggregateVars method for variable aggregation * Add knapsack function for modeling the knapsack problem * Add parameter settings to disable automatic presolvers and propagators in the knapsack model * Add ShiftboundPresolver for variable domain transformation in SCIP * Add tests for Shiftbound presolver with parametrised knapsack instances * Update docstring in shiftbound.py to clarify presolver example and its functionality * Add tests for Model.aggregateVars to verify aggregation functionality * Add test for aggregation infeasibility in binary variables * Remove Shiftbound presolver tests from test_shiftbound.py * Refactor TODO comment in test_isActive to clarify missing test cases for fixed and aggregated variables * Update CHANGELOG to include new features: isActive(), aggregateVars(), and example shiftbound.py * Add tutorial for writing a custom presolver using PySCIPOpt * Add tutorial for presolver plugin to CHANGELOG * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review * Clarify comments in the Shiftbound presolver example for better understanding of variable aggregation logic * change file name * wrap new methods and add tests * slight changes in docs and example * add missing method stubs for adjustedVarLb, adjustedVarUb, aggregateVars, isIntegral * Address review comments on presolver PR --------- Co-authored-by: fvz185 <> Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Mohammed Ghannam <ghannam@zib.de> Co-authored-by: Joao-Dionisio <joao.goncalves.dionisio@gmail.com>
1 parent b1e47ac commit 16580f6

File tree

8 files changed

+791
-1
lines changed

8 files changed

+791
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@
8080
- Added enableDebugSol() and disableDebugSol() for controlling the debug solution mechanism if DEBUGSOL=true
8181
- Added getVarPseudocostScore() and getVarPseudocost()
8282
- Added getNBranchings() and getNBranchingsCurrentRun()
83+
- Added isActive() which wraps SCIPvarIsActive() and test
84+
- Added aggregateVars() and tests
85+
- Added example shiftbound.py
86+
- Added a tutorial in ./docs on the presolver plugin
8387
### Fixed
8488
- Raised an error when an expression is used when a variable is required
8589
- Fixed some compile warnings

docs/tutorials/presolver.rst

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
###########
2+
Presolvers
3+
###########
4+
5+
For the following, let us assume that a Model object is available, which is created as follows:
6+
7+
.. code-block:: python
8+
9+
from pyscipopt import Model, Presol, SCIP_RESULT, SCIP_PRESOLTIMING
10+
11+
scip = Model()
12+
13+
.. contents:: Contents
14+
----------------------
15+
16+
17+
What is Presolving?
18+
===================
19+
20+
Presolving simplifies a problem before the actual search starts. Typical
21+
transformations include:
22+
23+
- tightening bounds,
24+
- removing redundant variables/constraints,
25+
- aggregating variables,
26+
- detecting infeasibility early.
27+
28+
This can reduce numerical issues and simplify constraints and objective
29+
expressions without changing the solution space.
30+
31+
32+
The Presol Plugin Interface (Python)
33+
====================================
34+
35+
A presolver in PySCIPOpt is a subclass of ``pyscipopt.Presol`` that implements the method:
36+
37+
- ``presolexec(self, nrounds, presoltiming)``
38+
39+
and is registered on a ``pyscipopt.Model`` via
40+
the class method ``pyscipopt.Model.includePresol``.
41+
42+
Here is a high-level flow:
43+
44+
1. Create subclass ``MyPresolver`` and capture any parameters in ``__init__``.
45+
2. Implement ``presolexec``: inspect variables, compute transformations, call SCIP aggregation APIs, and return a result code.
46+
3. Register your presolver using ``includePresol`` with a priority, maximal rounds, and timing.
47+
4. Solve the model, e.g. by calling ``presolve`` or ``optimize``.
48+
49+
50+
A Minimal Skeleton
51+
------------------
52+
53+
.. code-block:: python
54+
55+
from pyscipopt import Presol, SCIP_RESULT
56+
57+
class MyPresolver(Presol):
58+
def __init__(self, someparam=123):
59+
self.someparam = someparam
60+
61+
def presolexec(self, nrounds, presoltiming):
62+
scip = self.model
63+
64+
# ... inspect model, change bounds, aggregate variables, etc. ...
65+
66+
return {"result": SCIP_RESULT.SUCCESS} # or DIDNOTFIND, DIDNOTRUN, CUTOFF
67+
68+
69+
Example: Writing a Custom Presolver
70+
===================================
71+
72+
This tutorial shows how to write a presolver entirely in Python using
73+
PySCIPOpt's ``Presol`` plugin interface. We will implement a small
74+
presolver that shifts variable bounds from ``[a, b]`` to ``[0, b - a]``
75+
and optionally flips signs to reduce constant offsets.
76+
77+
For educational purposes, we keep our example as close as possible to SCIP's implementation, which can be found `here <https://scipopt.org/doc-5.0.1/html/presol__boundshift_8c_source.php>`__. However, one may implement Boundshift differently, as SCIP's logic does not translate perfectly to Python. To avoid any confusion with the already implemented version of Boundshift, we will call our custom presolver *Shiftbound*.
78+
79+
A complete working example can be found in the directory:
80+
81+
- ``examples/finished/presol_shiftbound.py``
82+
83+
84+
Implementing Shiftbound
85+
-----------------------
86+
87+
Below we walk through the important parts to illustrate design decisions to translate the Boundshift presolver to PySCIPOpt.
88+
89+
We want to provide parameters to control the presolver's behaviour:
90+
91+
- ``maxshift``: maximum length of interval ``b - a`` we are willing to shift,
92+
- ``flipping``: allow sign flips for better numerics,
93+
- ``integer``: only shift integer-ranged variables if true.
94+
95+
We will put these parameters into the ``__init__`` method to help us initialise the attributes of the presolver class. Then, in ``presolexec``, we implement the algorithm our custom presolver must follow.
96+
97+
.. code-block:: python
98+
99+
from pyscipopt import SCIP_RESULT, Presol
100+
101+
class ShiftboundPresolver(Presol):
102+
def __init__(self, maxshift=float("inf"), flipping=True, integer=True):
103+
self.maxshift = maxshift
104+
self.flipping = flipping
105+
self.integer = integer
106+
107+
def presolexec(self, nrounds, presoltiming):
108+
scip = self.model
109+
110+
# Respect global presolve switches (here, if aggregation disabled)
111+
if scip.getParam("presolving/donotaggr"):
112+
return {"result": SCIP_RESULT.DIDNOTRUN}
113+
114+
# We want to operate on non-binary active variables only
115+
scipvars = scip.getVars()
116+
nbin = scip.getNBinVars()
117+
vars = scipvars[nbin:] # SCIP orders by type: binaries first
118+
119+
result = SCIP_RESULT.DIDNOTFIND
120+
121+
for var in reversed(vars):
122+
assert var.vtype() != "BINARY" # already excluded by slicing
123+
if not var.isActive():
124+
continue
125+
126+
lb = var.getLbGlobal()
127+
ub = var.getUbGlobal()
128+
129+
# For integral types: round to feasible integers to avoid noise
130+
if var.vtype() != "CONTINUOUS":
131+
assert scip.isIntegral(lb)
132+
assert scip.isIntegral(ub)
133+
lb = scip.adjustedVarLb(var, lb)
134+
ub = scip.adjustedVarUb(var, ub)
135+
136+
# Is the variable already fixed?
137+
if scip.isEQ(lb, ub):
138+
continue
139+
140+
# If demanded by the parameters, restrict to integral-length intervals
141+
if self.integer and not scip.isIntegral(ub - lb):
142+
continue
143+
144+
# Only shift "reasonable" finite bounds
145+
MAXABSBOUND = 1000.0
146+
shiftable = all((
147+
not scip.isEQ(lb, 0.0),
148+
scip.isLT(ub, scip.infinity()),
149+
scip.isGT(lb, -scip.infinity()),
150+
scip.isLT(ub - lb, self.maxshift),
151+
scip.isLE(abs(lb), MAXABSBOUND),
152+
scip.isLE(abs(ub), MAXABSBOUND),
153+
))
154+
if not shiftable:
155+
continue
156+
157+
# Create a new variable y with bounds [0, ub-lb], and same type
158+
newvar = scip.addVar(
159+
name=f"{var.name}_shift",
160+
vtype=var.vtype(),
161+
lb=0.0,
162+
ub=(ub - lb),
163+
obj=0.0,
164+
)
165+
166+
# Aggregate old variable with new variable:
167+
# var + newvar = ub (flip case, when |ub| < |lb|)
168+
# var - newvar = lb (no flip case)
169+
if self.flipping and (abs(ub) < abs(lb)):
170+
infeasible, redundant, aggregated = scip.aggregateVars(var, newvar, 1.0, 1.0, ub)
171+
else:
172+
infeasible, redundant, aggregated = scip.aggregateVars(var, newvar, 1.0, -1.0, lb)
173+
174+
# Has the problem become infeasible?
175+
if infeasible:
176+
return {"result": SCIP_RESULT.CUTOFF}
177+
178+
# Aggregation succeeded; SCIP marks var as redundant and keeps newvar for further search
179+
assert redundant
180+
assert aggregated
181+
result = SCIP_RESULT.SUCCESS
182+
183+
return {"result": result}
184+
185+
Registering the Presolver
186+
-------------------------
187+
188+
After having initialised our ``model``, we instantiate an object based on our ``ShiftboundPresolver`` including the parameters we wish our presolver's behaviour to be set to.
189+
Lastly, we register the custom presolver by including ``presolver``, followed by a name and a description, as well as specifying its priority, maximum rounds to be called (where ``-1`` specifies no limit), and timing mode.
190+
191+
.. code-block:: python
192+
193+
from pyscipopt import Model, SCIP_PRESOLTIMING, SCIP_PARAMSETTING
194+
195+
model = Model()
196+
197+
presolver = ShiftboundPresolver(maxshift=float("inf"), flipping=True, integer=True)
198+
model.includePresol(
199+
presolver,
200+
"shiftbound",
201+
"converts variables with domain [a,b] to variables with domain [0,b-a]",
202+
priority=7900000,
203+
maxrounds=-1,
204+
timing=SCIP_PRESOLTIMING.FAST,
205+
)

0 commit comments

Comments
 (0)