@@ -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)
0 commit comments