Skip to content

Commit facf66f

Browse files
authored
Merge pull request #305 from ds4dm/feature-solve-iter
Feature solve iter
2 parents cef7794 + 914d8cc commit facf66f

File tree

32 files changed

+1238
-547
lines changed

32 files changed

+1238
-547
lines changed

dev/conda.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ dependencies:
2323

2424
# Documentation
2525
- doxygen
26-
- sphinx = 4.0
26+
- sphinx = 4.4
2727
- breathe>=4.15
2828
- sphinx_rtd_theme
2929

docs/discussion/seeding.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ One such aspect is the solver randomness, which is controlled by its random seed
88

99
This means that, by default, Ecole environment will generate different episodes (and in
1010
particular different initial states) after each new call to
11-
:py:meth:`~ecole.environment.EnvironmentComposer.reset`.
11+
:py:meth:`~ecole.environment.Environment.reset`.
1212
To do so, the environment keeps a :py:class:`~ecole.RandomGenerator` (random state)
1313
between episodes, and start a new episode by calling
1414
:py:meth:`~ecole.typing.Dynamics.set_dynamics_random_state` on the underlying

docs/howto/create-environments.rst

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,114 @@ To do so, we will take parameters in the constructor.
117117
__Dynamics__ = SimpleBranchingDynamics
118118

119119

120-
The constructor arguments are forwarded from the :py:meth:`~ecole.environment.EnvironmentComposer.__init__` constructor:
120+
The constructor arguments are forwarded from the :py:meth:`~ecole.environment.Environment.__init__` constructor:
121121

122122
.. testcode::
123123
:skipif: pyscipopt is None
124124

125125
env = SimpleBranching(observation_function=None, disable_cuts=False)
126126

127-
Similarily, extra arguments given to the environemnt :py:meth:`~ecole.environment.EnvironmentComposer.reset` and
128-
:py:meth:`~ecole.environment.EnvironmentComposer.step` are forwarded to the associated
127+
Similarily, extra arguments given to the environemnt :py:meth:`~ecole.environment.Environment.reset` and
128+
:py:meth:`~ecole.environment.Environment.step` are forwarded to the associated
129129
:py:class:`~ecole.typing.Dynamics` methods.
130+
131+
Using Control Inversion
132+
-----------------------
133+
When using a traditional SCIP callback, the user has to add the callback to SCIP, call ``SCIPsolve``, and wait for the
134+
solving process to terminate.
135+
We say that *SCIP has the control*.
136+
This has some downsides, such a having to forward all the data the agent will use to the callback, making it harder to
137+
stop the solving process, and reduce interactivity.
138+
For instance when using a callback in a notebook, if the user forgot to fetch some data, then they have to re-execute
139+
the whole solving process.
140+
141+
On the contrary, when using an Ecole environment such as :py:class:`~ecole.environment.Branching`, the environment
142+
pauses on every branch-and-bound node (*i.e.* every branchrule callback call) to let the user make a decision,
143+
or inspect the :py:class:`~ecole.scip.Model`.
144+
We say that the *user (or the agent) has the control*.
145+
To do so, we did not reconstruct the solving algorithm ``SCIPsolve`` to fit our needs.
146+
Rather, we have implemented a general *inversion of control* mechanism to let SCIP pause and be resumed on every
147+
callback call (using a form of *stackful coroutine*).
148+
We call this approach *iterative solving* and it runs exactly the same ``SCIPsolve`` algorithm, without noticable
149+
overhead, while perfectly forwarding all information available in the callback.
150+
151+
To use this tool, the user start by calling :py:meth:`ecole.scip.Model.solve_iter`, with a set of call callback
152+
constructor arguments.
153+
Iterative solving will then add these callbacks, start solving, and return the first time that one of these callback
154+
is executed.
155+
The return value describes where the solving has stopped, and the parameters of the callback where it has stopped.
156+
This is the time for the user to perform whichever action they would have done in the callback.
157+
Solving can be resumed by calling :py:meth:`ecole.scip.Model.solve_iter_continue` with the
158+
:py:class:`ecole.scip.callback.Result` that would have been set in the callback.
159+
Solving is finished when one of the iterative solving function returns ``None``.
160+
The :py:class:`ecole.scip.Model` can safely be deleted an any time (SCIP termination is handled automatically).
161+
162+
For instance, iterative solving an environement while pausing on branchrule and heuristic callbacks look like the
163+
following.
164+
165+
.. testcode::
166+
167+
model = ecole.scip.Model.from_file("path/to/file")
168+
169+
# Start solving until the first pause, if any.
170+
fcall = model.solve_iter(
171+
# Stop on branchrule callback.
172+
ecole.scip.callback.BranchruleConstructor(),
173+
# Stop on heuristic callback after node.
174+
ecole.scip.callback.HeuristicConstructor(timing_mask=ecole.scip.HeurTiming.AfterNode),
175+
)
176+
# While solving is not finished, `fcall` contains information about the current stop.
177+
while fcall is not None:
178+
# Solving stopped on a branchrule callback.
179+
if isinstance(fcall, ecole.scip.callback.BranchruleCall):
180+
# Perform some branching (through PyScipOpt).
181+
...
182+
# Resume solving until next pause.
183+
fcall = model.solve_iter_continue(ecole.scip.callback.Result.Branched)
184+
# Solving stopped on a heurisitc callback.
185+
elif isinstance(fcall, ecole.scip.callback.HeuristicCall):
186+
# Return as no heuristic was performed (only data collection)
187+
fcall = model.solve_iter_continue(ecole.scip.callback.Result.DidNotRun)
188+
189+
See :py:class:`~ecole.scip.callback.BranchruleConstructor`, :py:class:`~ecole.scip.callback.HeuristicConstructor` for
190+
callback constructor parameters, as well as :py:class:`~ecole.scip.callback.BranchruleCall` and
191+
:py:class:`~ecole.scip.callback.BranchruleCall` for callbacks functions parameters passed by SCIP to the callback
192+
methods.
193+
194+
.. note::
195+
196+
By default callback parameters such as ``priority``, ``frequency``, and ``max_depth`` taht control how when
197+
the callback are evaluated by SCIP are set to run as often as possible.
198+
However, it is entirely possible to run it with lower priority or frequency for create specific environments or
199+
whatever other purpose.
200+
201+
To create dynamics using iterative solving, one should call :py:meth:`ecole.scip.Model.solve_iter` in
202+
:py:meth:`~ecole.typing.Dynamics.reset_dynamics` and :py:meth:`ecole.scip.Model.solve_iter_continue` in
203+
:py:meth:`~ecole.typing.Dynamics.step_dynamics`.
204+
For instance, a branching environment could be created with the following dynamics.
205+
206+
.. testcode::
207+
:skipif: pyscipopt is None
208+
209+
class MyBranchingDynamics:
210+
def __init__(self, pseudo_candidates=False, max_depth=ecole.scip.callback.max_depth_none):
211+
self.pseudo_candidates = pseudo_candidates
212+
self.max_depth = max_depth
213+
214+
def action_set(self, model):
215+
if self.pseudo_candidates:
216+
return model.as_pyscipopt().getPseudoBranchCands()
217+
else:
218+
return model.as_pyscipopt().getLPBranchCands()
219+
return ...
220+
221+
def reset_dynamics(self, model):
222+
fcall = model.solve_iter(
223+
ecole.scip.callback.BranchruleConstructor(max_depth=self.max_depth)
224+
)
225+
return (fcall is None), self.action_set(model)
226+
227+
def step_dynamics(self, model, action):
228+
model.as_pyscipopt().branchVar(action)
229+
fcall = model.solve_iter_continue(ecole.scip.callback.Result.Branched)
230+
return (fcall is None), self.action_set(model)

docs/howto/instances.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ With an Environment
5959
-------------------
6060
The instance objects generated by :py:class:`~ecole.typing.InstanceGenerator`s,
6161
of type :py:class:`ecole.scip.Model`, can be passed directly to an environment's
62-
:py:meth:`~ecole.environment.EnvironmentComposer.reset` method.
62+
:py:meth:`~ecole.environment.Environment.reset` method.
6363

6464
A typical example training over 1000 instances/episodes would look like:
6565

@@ -78,7 +78,7 @@ A typical example training over 1000 instances/episodes would look like:
7878

7979
.. note::
8080
The generated instance objects can be, in principle, modified between their generation and their usage in an environment
81-
:py:meth:`~ecole.environment.EnvironmentComposer.reset` method. To keep code clean, however, we recommend that such modifications
81+
:py:meth:`~ecole.environment.Environment.reset` method. To keep code clean, however, we recommend that such modifications
8282
be wrapped in a custom environment class. Details about custom environments :ref:`can be found here<create-new-environment>`.
8383

8484

docs/reference/scip-interface.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,31 @@ SCIP Interface
44
Model
55
-----
66
.. autoclass:: ecole.scip.Model
7+
8+
Callbacks
9+
---------
10+
Branchrule
11+
^^^^^^^^^^
12+
.. autoclass:: ecole.scip.callback.BranchruleConstructor
13+
.. autoclass:: ecole.scip.callback.BranchruleCall
14+
15+
Heuristic
16+
^^^^^^^^^
17+
.. autoclass:: ecole.scip.callback.HeuristicConstructor
18+
.. autoclass:: ecole.scip.callback.HeuristicCall
19+
20+
Utilities
21+
^^^^^^^^^
22+
.. autoattribute:: ecole.scip.callback.priority_max
23+
.. autoattribute:: ecole.scip.callback.max_depth_none
24+
.. autoattribute:: ecole.scip.callback.max_bound_distance_none
25+
.. autoattribute:: ecole.scip.callback.frequency_always
26+
.. autoattribute:: ecole.scip.callback.frequency_offset_none
27+
28+
.. autoclass:: ecole.scip.callback.Result
29+
.. autoclass:: ecole.scip.callback.Type
30+
31+
SCIP Data Types
32+
---------------
33+
.. autoclass:: ecole.scip.Stage
34+
.. autoclass:: ecole.scip.HeurTiming

libecole/CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ add_library(
55
src/random.cpp
66
src/exception.cpp
77

8-
src/utility/reverse-control.cpp
98
src/utility/chrono.cpp
109
src/utility/graph.cpp
1110

libecole/include/ecole/dynamics/primal-search.hpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
#include <nonstd/span.hpp>
88
#include <scip/def.h>
9-
#include <scip/scip_heur.h>
109
#include <scip/type_result.h>
1110
#include <xtensor/xtensor.hpp>
1211

@@ -27,7 +26,7 @@ class ECOLE_EXPORT PrimalSearchDynamics : public DefaultSetDynamicsRandomState {
2726

2827
using DefaultSetDynamicsRandomState::set_dynamics_random_state;
2928

30-
ECOLE_EXPORT auto reset_dynamics(scip::Model& model) -> std::tuple<bool, ActionSet>;
29+
ECOLE_EXPORT auto reset_dynamics(scip::Model& model) const -> std::tuple<bool, ActionSet>;
3130

3231
ECOLE_EXPORT auto step_dynamics(scip::Model& model, Action action) -> std::tuple<bool, ActionSet>;
3332

@@ -38,7 +37,6 @@ class ECOLE_EXPORT PrimalSearchDynamics : public DefaultSetDynamicsRandomState {
3837
int depth_stop;
3938

4039
unsigned int trials_spent = 0; // to keep track of the number of trials during each search
41-
SCIP_HEUR* heur = nullptr; // to tell SCIP where primal solutions come from
4240
SCIP_RESULT result = SCIP_DIDNOTRUN; // the final result of each search (several trials)
4341
};
4442

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#pragma once
2+
3+
#include <tuple>
4+
#include <variant>
5+
6+
#include <scip/type_timing.h>
7+
8+
#include "ecole/utility/unreachable.hpp"
9+
10+
/**
11+
* Reverse callback tools.
12+
*
13+
* Helper tools for using reverse callback for iterative solving.
14+
*/
15+
namespace ecole::scip::callback {
16+
17+
/** Type of rverse callback available. */
18+
enum struct Type { Branchrule, Heuristic };
19+
20+
/** Return the name used for the reverse callback. */
21+
constexpr auto name(Type type) {
22+
switch (type) {
23+
case Type::Branchrule:
24+
return "ecole::scip::StopLocation::Branchrule";
25+
case Type::Heuristic:
26+
return "ecole::scip::StopLocation::Heuristic";
27+
default:
28+
utility::unreachable();
29+
}
30+
}
31+
32+
constexpr inline int priority_max = 536870911;
33+
constexpr inline int max_depth_none = -1;
34+
constexpr inline double max_bound_distance_none = 1.0;
35+
constexpr inline int frequency_always = 1;
36+
constexpr inline int frequency_offset_none = 0;
37+
38+
/** Parameter passed to create a reverse callback. */
39+
template <Type type> struct Constructor;
40+
41+
/** Parameter passed to a reverse branchrule. */
42+
template <> struct Constructor<Type::Branchrule> {
43+
int priority = priority_max;
44+
int max_depth = max_depth_none;
45+
double max_bound_distance = max_bound_distance_none;
46+
};
47+
using BranchruleConstructor = Constructor<Type::Branchrule>;
48+
49+
/** Parameter passed to create a reverse heurisitc. */
50+
template <> struct Constructor<Type::Heuristic> {
51+
int priority = priority_max;
52+
int frequency = frequency_always;
53+
int frequency_offset = frequency_offset_none;
54+
int max_depth = max_depth_none;
55+
SCIP_HEURTIMING timing_mask = SCIP_HEURTIMING_AFTERNODE;
56+
};
57+
using HeuristicConstructor = Constructor<Type::Heuristic>;
58+
59+
using DynamicConstructor = std::variant<Constructor<Type::Branchrule>, Constructor<Type::Heuristic>>;
60+
61+
/** Parameter given by SCIP to the callback function. */
62+
template <Type type> struct Call;
63+
64+
/** Parameter given by SCIP to the branchrule function. */
65+
template <> struct Call<Type::Branchrule> {
66+
/** The method of the Branchrule callback being called. */
67+
enum struct Where { LP, External, Pseudo };
68+
69+
bool allow_add_constraints;
70+
Where where;
71+
};
72+
using BranchruleCall = Call<Type::Branchrule>;
73+
74+
/** Parameter given by SCIP to the heuristic functions. */
75+
template <> struct Call<Type::Heuristic> {
76+
SCIP_HEURTIMING heuristic_timing;
77+
bool node_infeasible;
78+
};
79+
using HeuristicCall = Call<Type::Heuristic>;
80+
81+
using DynamicCall = std::variant<Call<Type::Branchrule>, Call<Type::Heuristic>>;
82+
83+
} // namespace ecole::scip::callback

0 commit comments

Comments
 (0)