|
| 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