This repository has been archived on 2025-03-06. You can view files and clone it, but cannot push or open issues or pull requests.
dekker-phd-thesis/chapters/6_incremental.tex

758 lines
39 KiB
TeX

\chapter{Incremental Processing}\label{ch:incremental}
%************************************************
Many applications require solving almost the same combinatorial problem
repeatedly, with only slight modifications, thousands of times. For example:
\begin{itemize}
\item Multi-objective problems~\autocite{jones-2002-multi-objective} are often
not supported directly in solvers. They can be solved using a
meta-search approach: find a solution to a (single-objective) problem,
then add more constraints to the original problem and repeat.
\item Large Neighbourhood Search~\autocite{shaw-1998-local-search} is a very
successful meta-heuristic. After finding a (sub-optimal) solution to a
problem, constraints are added to restrict the search in the
neighbourhood of that solution. When a new solution is found, the
constraints are removed, and constraints for a new neighbourhood are
added.
\item In Online Optimisation~\autocite{jaillet-2021-online}, a problem
instance is continuously updated with new data, such as newly available
jobs to be scheduled or customer requests to be processed.
\item Diverse Solution Search~\autocite{hebrard-2005-diverse} aims at
providing a set of solutions that are sufficiently different from each
other, in order to give human decision makers an overview of the
solution space. Diversity can be achieved by repeatedly solving a
problem instance with different objectives.
\item In Interactive Search~\autocite{}, a user provides feedback
on decisions made by the solver. The feedback is added back into the
problem, and a new solution is generated. Users may also take back some
earlier feedback and explore different aspects of the problem.
\end{itemize}
All of these examples have in common that a problem instance is solved, new
constraints are added, the resulting instance is solved again, and constraints
may be removed again.
The usage of these methods is not new to \minizinc\ and they have proven to be
very useful \autocite{rendl-2015-minisearch, schrijvers-2013-combinators,
dekker-2018-mzn-lns, schiendorfer-2018-minibrass}. In its most basic form, a
simple scripting language is sufficient to implement these methods, by
repeatedly calling on \minizinc\ to flatten and solve the updated problems.
While the techniques presented so far in this paper should already improve the
performance of these approaches, the overhead of re-flattening an almost
identical model may still prove prohibitive, warranting direct support for
adding and removing constraints in the \minizinc\ abstract machine. In this
section, we will see that our proposed architecture can be made
\emph{incremental}, significantly improving efficiency for these iterative
solving approaches.
\section{Incremental Flattening}
In order to support incremental flattening, the \nanozinc\ interpreter must be
able to process \nanozinc\ calls \emph{added} to an existing \nanozinc\ program,
as well as to \emph{remove} calls from an existing \nanozinc\ program. Adding new
calls is straightforward, since \nanozinc\ is already processed call-by-call.
Removing a call, however, is not so simple. When we remove a call, all effects
the call had on the \nanozinc\ program have to be undone, including results of
propagation, CSE and other simplifications.
\begin{example}\label{ex:6-incremental}
Consider the following \minizinc\ fragment:
\highlightfile{assets/mzn/6_incremental.mzn}
After evaluating the first constraint, the domain of \mzninline{x} is changed to
be less than 10. Evaluating the second constraint causes the domain of
\mzninline{y} to be less than 9. If we now, however, try to remove the first
constraint, it is not just the direct inference on the domain of \mzninline{x}
that has to be undone, but also any further effects of those changes -- in this
case, the changes to the domain of \mzninline{y}.
\end{example}
Due to this complex interaction between calls, we only support the removal of
calls in reverse chronological order, also known as \textit{backtracking}. The
common way of implementing backtracking is using a \textit{trail} data
structure~\autocite{warren-1983-wam}. The trail records all changes to the
\nanozinc\ program:
\begin{itemize}
\item the addition or removal of new variables or constraints,
\item changes made to the domains of variables,
\item additions to the CSE table, and
\item substitutions made due to equality propagation.
\end{itemize}
These changes can be caused by the evaluation of a call, propagation, or CSE.
When a call is removed, the corresponding changes can now be undone by
reversing any action recorded on the trail up to the point where the call was
added.
In order to limit the amount of trailing required, the programmer must create
explicit \textit{choice points} to which the system state can be reset. In
particular, this means that if no choice point was created before the initial
model was flattened, then this flattening can be performed without any
trailing.
\begin{example}\label{ex:6-trail}
Let us look again at the resulting \nanozinc\ code from \Cref{ex:absreif}:
% \highlightfile{assets/mzn/6_abs_reif_result.mzn}
Assume that we added a choice point before posting the constraint
\mzninline{c}. Then the trail stores the \emph{inverse} of all modifications
that were made to the \nanozinc\ as a result of \mzninline{c} (where
$\mapsfrom$ denotes restoring an identifier, and $\lhd$ \texttt{+}/\texttt{-}
respectively denote attaching and detaching constraints):
% \highlightfile{assets/mzn/6_abs_reif_trail.mzn}
To reconstruct the \nanozinc\ program at the choice point, we simply apply
the changes recorded in the trail, in reverse order.
\end{example}
\section{Incremental Solving}
Ideally, the incremental changes made by the interpreter would also be applied
incrementally to the solver. This requires the solver to support both the
dynamic addition and removal of variables and constraints. While some solvers
can support this functionality, most solvers have limitations. The system can
therefore support solvers with different levels of an incremental interface:
\begin{itemize}
\item Using a non-incremental interface, the solver is reinitialised with the
updated \nanozinc\ program every time. In this case, we still get a
performance benefit from the improved flattening time, but not from
incremental solving.
\item Using a \textit{warm-starting} interface, the solver is reinitialised
with the updated program as above, but it is also given a previous solution
to initialise some internal data structures. In particular for mathematical
programming solvers, this can result in dramatic performance gains compared
to ``cold-starting'' the solver every time.
\item Using a fully incremental interface, the solver is instructed to apply
the changes made by the interpreter. In this case, the trail data structure
is used to compute the set of \nanozinc\ changes since the last choice
point.
\end{itemize}
\section{Experiments}
We have created a prototype implementation of the architecture presented in the
preceding sections. It consists of a compiler from \minizinc\ to \microzinc, and
an incremental \microzinc\ interpreter producing \nanozinc. The system supports
a significant subset of the full \minizinc\ language; notable features that are
missing are support for set and float variables, option types, and compilation
of model output expressions and annotations. We will release our implementation
under an open-source license and can make it available to the reviewers upon
request.
The implementation is not optimised for performance yet, but was created as a
faithful implementation of the developed concepts, in order to evaluate their
suitability and provide a solid baseline for future improvements. In the
following we present experimental results on basic flattening performance as
well as incremental flattening and solving that demonstrate the efficiency
gains that are possible thanks to the new architecture.
\subsection{Incremental Flattening and Solving}
To demonstrate the advantage that the incremental processing of \minizinc\
offers, we present a runtime evaluation of two meta-heuristics implemented using
our prototype interpreter. For both meta-heuristics, we evaluate the performance
of fully re-evaluating and solving the instance from scratch, compared to the
fully incremental evaluation and solving. The solving in both tests is performed
by the Gecode solver, version 6.1.2, connected using the fully incremental API.
\paragraph{GBAC}
The Generalised Balanced Academic Curriculum (GBAC) problem
\autocite{chiarandini-2012-gbac} is comprised of scheduling the courses in a
curriculum subject to load limits on the number of courses for each period,
prerequisites for courses, and preferences of teaching periods by teaching
staff. It has been shown~\autocite{dekker-2018-mzn-lns} that Large Neighbourhood
Search (LNS) is a useful meta-heuristic for quickly finding high quality
solutions to this problem. In LNS, once an initial (sub-optimal) solution is
found, constraints are added to the problem that restrict the search space to a
\textit{neighbourhood} of the previous solution. After this neighbourhood has
been explored, the constraints are removed, and constraints for a different
neighbourhood are added. This is repeated until a sufficiently high solution
quality has been reached.
We can model a neighbourhood in \minizinc\ as a predicate that, given the values
of the variables in the previous solution, posts constraints to restrict the
search. The following predicate defines a suitable neighbourhood for the GBAC
problem:
\highlightfile{assets/mzn/6_gbac_neighbourhood.mzn}
When this predicate is called with a previous solution \mzninline{sol}, then
every \mzninline{period_of} variable has an $80\%$ chance to be fixed to its
value in the previous solution. With the remaining $20\%$, the variable is
unconstrained and will be part of the search for a better solution.
In a non-incremental architecture, we would re-flatten the original model plus
the neighbourhood constraint for each iteration of the LNS. In the incremental
\nanozinc\ architecture, we can easily express LNS as a repeated addition and
retraction of the neighbourhood constraints. We implemented both approaches
using the \nanozinc\ prototype, with the results shown in \Cref{fig:gbac}. The
incremental \nanozinc\ translation shows a 12x speedup compared to re-compiling
the model from scratch in each iteration. For this particular problem,
incrementality in the target solver (Gecode) does not lead to a significant
reduction in runtime.
\begin{figure}
\centering
\includegraphics[width=0.5\columnwidth]{assets/img/6_gbac}
\caption{\label{fig:gbac}A run-time performance comparison between incremental
processing (Incr.) and re-evaluation (Redo) of 5 GBAC \minizinc\ instances
in the application of LNS on a 3.4 GHz Quad-Core Intel Core i5 using the
Gecode 6.1.2 solver. Each run consisted of 2500 iterations of applying
neighbourhood predicates. Reported times are averages of 10 runs.}
\end{figure}
\paragraph{Radiation}
Our second experiment is based on a problem of planning cancer radiation therapy
treatment using multi-leaf collimators \autocite{baatar-2011-radiation}. Two
characteristics mark the quality of a solution: the amount of time the patient
is exposed to radiation, and the number of ``shots'' or different angles the
treatment requires. However, the first characteristic is considered more
important than the second. The problem therefore has a lexicographical
objective: a solution is better if it requires a strictly shorter exposure time,
or the same exposure time but a lower number of ``shots''.
\minizinc\ solvers do not support lexicographical objectives directly, but we
can instead repeatedly solve a model instance and add a constraint to ensure
that the lexicographical objective improves. When the solver proves that no
better solution can be found, the last solution is known to be optimal. Given
two variables \mzninline{exposure} and \mzninline{shots}, once we have found a
solution with \mzninline{exposure=e} and \mzninline{shots=s}, we can add the
constraint \mzninline{exposure < e \/ (exposure = e /\ shots < s)}, expressing
the lexicographic ordering, and continue the search. Since each added
lexicographic constraint is strictly stronger than the previous one, we never
have to retract previous constraints.
\begin{figure}
\centering
\includegraphics[width=0.5\columnwidth]{assets/img/6_radiation}
\caption{\label{fig:radiation}A run-time performance comparison between
incremental processing (Incr.) and re-evaluation (Redo) of 9 Radiation
\minizinc\ instances in the application of Lexicographic objectives on a 3.4
GHz Quad-Core Intel Core i5 using the Gecode 6.1.2 solver. Each test was run
to optimality and was conducted 20 times to provide an average.}
\end{figure}
As shown in \cref{fig:radiation}, the incremental processing of the added
\mzninline{lex_less} calls is a clear improvement over the re-evaluation of the
whole model. The translation shows a 13x speedup on average, and even the time
spent solving is reduced by 33\%.
\section{Modelling of Neighbourhoods and Meta-heuristics}
\label{section:2-modelling-nbhs}
%
% Start with a brief review of most common neighbourhoods and then explain:
% \begin{itemize}
% \item Random built in with integers
% \begin{itemize}
% \item Explain the built in
% \item Give an example of use (use model)
% \item Limitations if any
% \end{itemize}
% \item Solution based one
% \begin{itemize}
% \item Explain the built in
% \item Give an example of use (use model)
% \item Limitations if any
% \end{itemize}
% \end{itemize}
% End with future work for other built ins (hint which ones would be useful).
Most LNS literature discusses neighbourhoods in terms of ``destroying'' part of
a solution that is later repaired. However, from a declarative modelling point
of view, it is more natural to see neighbourhoods as adding new constraints and
variables that need to be applied to the base model, \eg forcing variables to
take the same value as in the previous solution.
This section introduces a \minizinc\ extension that enables modellers to define
neighbourhoods using the $\mathit{nbh(a)}$ approach described above. This
extension is based on the constructs introduced in
\minisearch\~\autocite{rendl-2015-minisearch}, as summarised below.
\subsection{LNS in \glsentrytext{minisearch}}
\minisearch\ introduced a \minizinc\ extension that enables modellers to express
meta-searches inside a \minizinc\ model. A meta-search in \minisearch\ typically
solves a given \minizinc\ model, performs some calculations on the solution,
adds new constraints and then solves again.
An LNS definition in \minisearch\ consists of two parts. The first part is a
declarative definition of a neighbourhood as a \minizinc\ predicate that posts
the constraints that should be added with respect to a previous solution. This
makes use of the \minisearch\ function: \mzninline{function int: sol(var int:
x)}, which returns the value that variable \mzninline{x} was assigned to in
the previous solution (similar functions are defined for Boolean, float and set
variables). In addition, a neighbourhood predicate will typically make use of
the random number generators available in the \minizinc\ standard library.
\Cref{lst:6-lns-minisearch-pred} shows a simple random neighbourhood. For each
decision variable \mzninline{x[i]}, it draws a random number from a uniform
distribution and, if it exceeds threshold \mzninline{destrRate}, posts
constraints forcing \mzninline{x[i]} to take the same value as in the previous
solution. For example, \mzninline{uniformNeighbourhood(x, 0.2)} would result in
each variable in the array \mzninline{x} having a 20\% chance of being
unconstrained, and an 80\% chance of being assigned to the value it had in the
previous solution.
\begin{listing}
\highlightfile{assets/mzn/6_lns_minisearch_pred.mzn}
\caption{\label{lst:6-lns-minisearch-pred} A simple random LNS predicate
implemented in \minisearch{}}
\end{listing}
\begin{listing}
\highlightfile{assets/mzn/6_lns_minisearch.mzn}
\caption{\label{lst:6-lns-minisearch} A simple LNS metaheuristic implemented
in \minisearch{}}
\end{listing}
The second part of a \minisearch\ LNS is the meta-search itself. The most basic
example is that of function \mzninline{lns} in \cref{lst:6-lns-minisearch}. It
performs a fixed number of iterations, each invoking the neighbourhood predicate
\mzninline{uniformNeighbourhood} in a fresh scope (so that the constraints only
affect the current loop iteration). It then searches for a solution
(\mzninline{minimize_bab}) with a given timeout, and if the search does return a
new solution, it commits to that solution (so that it becomes available to the
\mzninline{sol} function in subsequent iterations). The \texttt{lns} function
also posts the constraint \mzninline{obj < sol(obj)}, ensuring the objective
value in the next iteration is strictly better than that of the current
solution.
\paragraph{Limitations of the \minisearch\ approach.}
Although \minisearch\ enables the modeller to express \emph{neighbourhoods} in a
declarative way, the definition of the \emph{meta-search} is rather unintuitive
and difficult to debug, leading to unwieldy code for defining simple restarting
strategies. Furthermore, the \minisearch\ implementation requires either a close
integration of the backend solver into the \minisearch\ system, or it drives the
solver through the regular text-file based \flatzinc\ interface, leading to a
significant communication overhead.
To address these two issues for LNS, we propose to keep modelling neighbourhoods
as predicates, but define a small number of additional \minizinc\ built-in
annotations and functions that (a) allow us to express important aspects of the
meta-search in a more convenient way, and (b) enable a simple compilation scheme
that requires no additional communication with and only small, simple extensions
of the backend solver.
The approach we follow here is therefore to \textbf{extend \flatzinc}, such that
the definition of neighbourhoods can be communicated to the solver together with
the problem instance. This maintains the loose coupling of \minizinc\ and
solver, while avoiding the costly communication and cold-starting of the
black-box approach.
\subsection{Restart annotations}
Instead of the complex \minisearch\ definitions, we propose to add support for
simple meta-searches that are purely based on the notion of \emph{restarts}. A
restart happens when a solver abandons its current search efforts, returns to
the root node of the search tree, and begins a new exploration. Many CP solvers
already provide support for controlling their restarting behaviour, e.g.\ they
can periodically restart after a certain number of nodes, or restart for every
solution. Typically, solvers also support posting additional constraints upon
restarting (e.g Comet~\autocite{michel-2005-comet}) that are only valid for the
particular restart (i.e., they are ``retracted'' for the next restart).
In its simplest form, we can therefore implement LNS by specifying a
neighbourhood predicate, and annotating the \mzninline{solve} item to indicate
the predicate should be invoked upon each restart:
\mzninline{solve ::on_restart(myNeighbourhood) minimize cost;}
Note that \minizinc\ currently does not support passing functions or predicates
as arguments. Calling the predicate, as in
\mzninline{::on_restart(myNeighbourhood())}, would not have the correct
semantics, since the predicate needs to be called for \emph{each} restart. As a
workaround, we currently pass the name of the predicate to be called for each
restart as a string (see the definition of the new \mzninline{on_restart}
annotation in \cref{lst:6-restart-ann}).
The second component of our LNS definition is the \emph{restarting strategy},
defining how much effort the solver should put into each neighbourhood (i.e.,
restart), and when to stop the overall search.
We propose adding new search annotations to \minizinc\ to control this behaviour
(see \cref{lst:6-restart-ann}). The \mzninline{restart_on_solution} annotation
tells the solver to restart immediately for each solution, rather than looking
for the best one in each restart, while \mzninline{restart_without_objective}
tells it not to add branch-and-bound constraints on the objective. The other
\mzninline{restart_X} annotations define different strategies for restarting the
search when no solution is found. The \mzninline{timeout} annotation gives an
overall time limit for the search, whereas \mzninline{restart_limit} stops the
search after a fixed number of restarts.
\begin{listing}[t]
\highlightfile{assets/mzn/6_restart_ann.mzn}
\caption{\label{lst:6-restart-ann} New annotations to control the restarting
behaviour}
\end{listing}
\subsection{Neighbourhood selection}
It is often beneficial to use several neighbourhood definitions for a problem.
Different neighbourhoods may be able to improve different aspects of a solution,
at different phases of the search. Adaptive LNS \autocite{ropke-2006-adaptive,
pisinger-2007-heuristic}, which keeps track of the neighbourhoods that led to
improvements and favours them for future iterations, is the prime example for
this approach. A simpler scheme may apply several neighbourhoods in a
round-robin fashion.
In \minisearch\, adaptive or round-robin approaches can be implemented using
\emph{state variables}, which support destructive update (overwriting the value
they store). In this way, the \minisearch\ strategy can store values to be used
in later iterations. We use the \emph{solver state} instead, i.e., normal
decision variables, and define two simple built-in functions to access the
solver state \emph{of the previous restart}. This approach is sufficient for
expressing neighbourhood selection strategies, and its implementation is much
simpler.
\paragraph{State access and initialisation}
The state access functions are defined in \cref{lst:6-state-access}. Function
\mzninline{status} returns the status of the previous restart, namely:
\mzninline{START} (there has been no restart yet); \mzninline{UNSAT} (the
restart failed); \mzninline{SAT} (the restart found a solution); \mzninline{OPT}
(the restart found and proved an optimal solution); and \mzninline{UNKNOWN} (the
restart did not fail or find a solution). Function \mzninline{lastval} (which,
like \mzninline{sol}, has versions for all basic variable types) allows
modellers to access the last value assigned to a variable (the value is
undefined if \mzninline{status()=START}).
\begin{listing}[t]
\highlightfile{assets/mzn/6_state_access.mzn}
\caption{\label{lst:6-state-access} Functions for accessing previous solver
states}
\end{listing}
In order to be able to initialise the variables used for state access, we
reinterpret \mzninline{on_restart} so that the predicate is also called for the
initial search (i.e., before the first ``real'' restart) with the same
semantics, that is, any constraint posted by the predicate will be retracted for
the next restart.
\paragraph{Parametric neighbourhood selection predicates}
We define standard neighbourhood selection strategies as predicates that are
parametric over the neighbourhoods they should apply. For example, since
\mzninline{on_restart} now also includes the initial search, we can define a
strategy \mzninline{basic_lns} that applies a neighbourhood only if the current
status is not \mzninline{START}:
\mzninline{predicate basic_lns(var bool: nbh) = (status()!=START -> nbh);}
In order to use this predicate with the \mzninline{on_restart} annotation, we
cannot simply pass \mzninline{basic_lns(uniformNeighbourhood(x,0.2))}. First of
all, calling \mzninline{uniformNeighbourhood} like that would result in a
\emph{single} evaluation of the predicate, since \minizinc\ employs a
call-by-value evaluation strategy. Furthermore, the \mzninline{on_restart}
annotation only accepts the name of a nullary predicate. Therefore, users have
to define their overall strategy in a new predicate. \Cref{lst:6-basic-complete}
shows a complete example of a basic LNS model.
\begin{listing}[t]
\highlightfile{assets/mzn/6_basic_complete.mzn}
\caption{\label{lst:6-basic-complete} Complete LNS example}
\end{listing}
We can also define round-robin and adaptive strategies using these primitives.
%\paragraph{Round-robin LNS}
\Cref{lst:6-round-robin} defines a round-robin LNS meta-heuristic, which cycles
through a list of \mzninline{N} neighbourhoods \mzninline{nbhs}. To do this, it
uses the decision variable \mzninline{select}. In the initialisation phase
(\mzninline{status()=START}), \mzninline{select} is set to \mzninline{-1}, which
means none of the neighbourhoods is activated. In any following restart,
\mzninline{select} is incremented modulo \mzninline{N}, by accessing the last
value assigned in a previous restart (\mzninline{lastval(select)}). This will
activate a different neighbourhood for each restart
(\lref{line:6:roundrobin:post}).
\begin{listing}[t]
\highlightfile{assets/mzn/6_round_robin.mzn}
\caption{\label{lst:6-round-robin} A predicate providing the round robin
meta-heuristic}
\end{listing}
%\paragraph{Adaptive LNS}
For adaptive LNS, a simple strategy is to change the size of the neighbourhood
depending on whether the previous size was successful or not.
\Cref{lst:6-adaptive} shows an adaptive version of the
\mzninline{uniformNeighbourhood} that increases the number of free variables
when the previous restart failed, and decreases it when it succeeded, within the
bounds $[0.6,0.95]$.
\begin{listing}[t]
\highlightfile{assets/mzn/6_adaptive.mzn}
\caption{\label{lst:6-adaptive} A simple adaptive neighbourhood}
\end{listing}
\subsection{Meta-heuristics}
The LNS strategies we have seen so far rely on the default behaviour of
\minizinc\ solvers to use branch-and-bound for optimisation: when a new solution
is found, the solver adds a constraint to the remainder of the search to only
accept better solutions, as defined by the objective function in the
\mzninline{minimize} or \mzninline{maximize} clause of the \mzninline{solve}
item. When combined with restarts and LNS, this is equivalent to a simple
hill-climbing meta-heuristic.
We can use the constructs introduced above to implement alternative
meta-heuristics such as simulated annealing. In particular, we use
\mzninline{restart_without_objective} to tell the solver not to add the
branch-and-bound constraint on restart. It will still use the declared objective
to decide whether a new solution is the globally best one seen so far, and only
output those (to maintain the convention of \minizinc\ solvers that the last
solution printed at any point in time is the currently best known one).
%
With \mzninline{restart_without_objective}, the restart predicate is now
responsible for constraining the objective function. Note that a simple
hill-climbing (for minimisation) can still be defined easily in this context as:
{
\centering
\scriptsize
\highlightfile{assets/mzn/6_hill_climbing.mzn}
}
It takes advantage of the fact that the declared objective function is available
through the built-in variable \mzninline{_objective}.
%
A simulated annealing strategy is also easy to
express:
{
\centering
\scriptsize
\highlightfile{assets/mzn/6_simulated_annealing.mzn}
}
\section{Compilation of Neighbourhoods} \label{section:compilation}
The neighbourhoods defined in the previous section can be executed with
\minisearch\ by adding support for the \mzninline{status} and
\mzninline{lastval} built-in functions, and by defining the main restart loop.
The \minisearch{} evaluator will then call a solver to produce a solution, and
evaluate the neighbourhood predicate, incrementally producing new \flatzinc\ to
be added to the next round of solving.
%
While this is a viable approach, our goal is to keep the compiler and solver
separate, by embedding the entire LNS specification into the \flatzinc\ that is
passed to the solver.
%
This section introduces such a compilation approach. It only requires simple
modifications of the \minizinc\ compiler, and the compiled \flatzinc\ can be
executed by standard CP solvers with a small set of simple extensions.
\subsection{Compilation overview}
The neighbourhood definitions from the previous section have an important
property that makes them easy to compile to standard \flatzinc: they are defined
in terms of standard \minizinc\ expressions, with the exception of a few new
built-in functions. When the neighbourhood predicates are evaluated in the
\minisearch\ way, the \minisearch\ runtime implements those built-in functions,
computing the correct value whenever a predicate is evaluated.
%
Instead, the compilation scheme presented below uses a limited form of
\emph{partial evaluation}: parameters known at compile time will be fully
evaluated; those only known during the solving, such as the result of a call to
any of the new functions (\mzninline{sol}, \mzninline{status}, etc.), are
replaced by decision variables. This essentially \textbf{turns the new built-in
functions into constraints} that have to be supported by the target solver.
The neighbourhood predicate can then be added as a constraint to the model. The
evaluation is performed by hijacking the solver's own capabilities: It will
automatically perform the evaluation of the new functions by propagating the new
constraints.
To compile an LNS specification to standard \flatzinc, the \minizinc\ compiler
performs four simple steps:
\begin{enumerate}
\item Replace the annotation \mzninline{::on_restart("X")} with a call to
predicate \mzninline{X}.
\item Inside predicate \mzninline{X} and any other predicate called
recursively from \mzninline{X}: treat any call to built-in functions
\mzninline{sol}, \mzninline{status}, and \mzninline{lastval} as
returning a \mzninline{var} instead of a \mzninline{par} value; and
rename calls to random functions, e.g., \mzninline{uniform} to
\mzninline{uniform_nbh}, in order to distinguish them from their
standard library versions.
\item Convert any expression containing a call from step 2 to \mzninline{var}
to ensure the functions are compiled as constraints, rather than
statically evaluated by the \minizinc\ compiler.
\item Compile the resulting model using an extension of the \minizinc\
standard library that provides declarations for these built-in
functions, as defined below.
\end{enumerate}
These transformations will not change the code of many neighbourhood
definitions, since the built-in functions are often used in positions that
accept both parameters and variables. For example, the
\mzninline{uniformNeighbourhood} predicate from \cref{lst:6-lns-minisearch-pred}
uses \mzninline{uniform(0.0,1.0)} in an \mzninline{if} expression, and
\mzninline{sol(x[i])} in an equality constraint. Both expressions can be
translated to \flatzinc\ when the functions return a \mzninline{var}.
\subsection{Compiling the new built-ins}
We can compile models that contain the new built-ins by extending the \minizinc\
standard library as follows.
\paragraph{\mzninline{status}}
\Cref{lst:6-status} shows the definition of the \mzninline{status} function. It
simply replaces the functional form by a predicate \mzninline{status} (declared
in \lref{line:6:status}), which constrains its local variable argument
\mzninline{stat} to take the status value.
\begin{listing}[t]
\highlightfile{assets/mzn/6_status.mzn}
\caption{\label{lst:6-status} MiniZinc definition of the \mzninline{status} function}
\end{listing}
\paragraph{\mzninline{sol} and \mzninline{lastval}}
Since \mzninline{sol} is overloaded for different variable types and \flatzinc\
does not support overloading, we produce type-specific built-ins for every type
of solver variable (\mzninline{int_sol(x, xi)}, \mzninline{bool_sol(x, xi)},
etc.). The resolving of the \mzninline{sol} function into these specific
built-ins is done using an overloaded definition like the one shown
in~\Cref{lst:6-int-sol} for integer variables. If the value of the variable in
question becomes known at compile time, we use that value instead. Otherwise, we
replace the function call with a type specific \mzninline{int_sol} predicate,
which is the constraint that will be executed by the solver.
%
\begin{listing}[t]
\highlightfile{assets/mzn/6_sol_function.mzn}
\caption{\label{lst:6-int-sol} MiniZinc definition of the \mzninline{sol}
function for integer variables}
\end{listing}
%
To improve the compilation of the model further, we use the declared bounds of
the argument (\mzninline{lb(x)..ub(x)}) to constrain the variable returned by
\mzninline{sol}. This bounds information is important for the compiler to be
able to generate the most efficient \flatzinc\ code for expressions involving
\mzninline{sol}. The compilation of \mzninline{lastval} is similar to that for
\mzninline{sol}.
\paragraph{Random number functions}
Calls to the random number functions have been renamed by appending
\texttt{\_nbh}, so that the compiler does not simply evaluate them statically.
The definition of these new functions follows the same pattern as for
\mzninline{sol}, \mzninline{status}, and \mzninline{lastval}. The MiniZinc
definition of the \mzninline{uniform_nbh} function is shown in
\Cref{lst:6-int-rnd}.%
\footnote{Random number functions need to be marked as \mzninline{::impure} for
the compiler not to apply Common Subexpression Elimination
(CSE)~\autocite{stuckey-2013-functions} if they are called multiple times with
the same arguments.}%
Note that the function accepts variable arguments \mzninline{l} and
\mzninline{u}, so that it can be used in combination with other functions, such
as \mzninline{sol}.
\begin{listing}[t]
\highlightfile{assets/mzn/6_uniform_neighbourhood.mzn}
\caption{\label{lst:6-int-rnd} MiniZinc definition of the
\mzninline{uniform_nbh} function for floats}
\end{listing}
\subsection{Solver support for LNS \glsentrytext{flatzinc}}
We will now show the minimal extensions required from a solver to interpret the
new \flatzinc\ constraints and, consequently, to execute LNS definitions
expressed in \minizinc.
First, the solver needs to parse and support the restart annotations
of~\cref{lst:6-restart-ann}. Many solvers already support all this
functionality. Second, the solver needs to be able to parse the new constraints
\mzninline{status}, and all versions of \mzninline{sol}, \mzninline{lastval},
and random number functions like \mzninline{float_uniform}. In addition, for the
new constraints the solver needs to:
\begin{itemize}
\item \mzninline{status(s)}: record the status of the previous restart, and
fix \mzninline{s} to the recorded status.
\item \mzninline{sol(x,sx)} (variants): constrain \mzninline{sx} to be equal
to the value of \mzninline{x} in the incumbent solution. If there is no
incumbent solution, it has no effect.
\item \mzninline{lastval(x,lx)} (variants): constrain \mzninline{lx} to take
the last value assigned to \mzninline{x} during search. If no value was
ever assigned, it has no effect. Note that many solvers (in particular
SAT and LCG solvers) already track \mzninline{lastval} for their
variables for use in search. To support LNS a solver must at least track
the \emph{lastval} of each of the variables involved in such a
constraint. This is straightforward by using the \mzninline{lastval}
propagator itself. It wakes up whenever the first argument is fixed, and
updates the last value (a non-backtrackable value).
\item random number functions: fix their variable argument to a random number
in the appropriate probability distribution.
\end{itemize}
Importantly, these constraints need to be propagated in a way that their effects
can be undone for the next restart. Typically, this means the solver must not
propagate these constraints in the root node of the search.
Modifying a solver to support this functionality is straightforward if it
already has a mechanism for posting constraints during restarts. We have
implemented these extensions for both Gecode (110 new lines of code) and Chuffed
(126 new lines of code).
For example, consider the model from \cref{lst:6-basic-complete} again.
\Cref{lst:6-flat-pred} shows a part of the \flatzinc\ that arises from compiling
\mzninline{basic_lns(uniformNeighbourhood(x, 0.2))}, assuming that
\mzninline{index_set(x) = 1..n}.
\Lrefrange{line:6:status:start}{line:6:status:end} define a Boolean variable
\mzninline{b1} that is true iff the status is not \mzninline{START}. The second
block of code (\lrefrange{line:6:x1:start}{line:6:x1:end}) represents the
decomposition of the expression
\mzninline{(status() != START /\ uniform(0.0,1.0) > 0.2) -> x[1] = sol(x[1])}
%
which is the result of merging the implication from the \mzninline{basic_lns}
predicate with the \mzninline{if} expression from
\mzninline{uniformNeighbourhood}. The code first introduces and constrains a
variable for the random number, then adds two Boolean variables: \mzninline{b2}
is constrained to be true iff the random number is greater than 0.2; while
\mzninline{b3} is constrained to be the conjunction \mzninline{status()!=START
/\ uniform(0.0,1.0)>0.2}. \lref{line:6:x1} constrains \mzninline{x1} to be the
value of \mzninline{x[1]} in the previous solution. Finally, the half-reified
constraint in \lref{line:6:x1:end} implements \mzninline{b3 -> x[1]=sol(x[1])}.
We have omitted the similar code generated for \mzninline{x[2]} to
\mzninline{x[n]}. Note that the \flatzinc\ shown here has been simplified for
presentation.
\begin{listing}[t]
\highlightfile{assets/mzn/6_basic_complete_transformed.mzn}
\caption{\label{lst:6-flat-pred} \flatzinc\ that results from compiling \\
\mzninline{basic_lns(uniformNeighbourhood(x,0.2))}.}
\end{listing}
The first time the solver is invoked, it sets \mzninline{s} to 1
(\mzninline{START}). Propagation will fix \mzninline{b1} to \mzninline{false}
and \mzninline{b3} to \mzninline{false}. Therefore, the implication in
\lref{line:6:x1:end} is not activated, leaving \mzninline{x[1]} unconstrained.
The neighbourhood constraints are effectively switched off.
When the solver restarts, all of the special propagators are re-executed.
Now \mzninline{s} is not 1, and \mzninline{b1} will be set to
\mzninline{true}. The \mzninline{float_random} propagator assigns
\mzninline{rnd1} a new random value and, depending on whether it is greater
than \mzninline{0.2}, the Boolean variables \mzninline{b2}, and consequently
\mzninline{b3} will be assigned. If it is \mzninline{true}, the constraint
in line \lref{line:6:x1:end} will become active and assign \mzninline{x[1]}
to its value in the previous solution.
Furthermore, it is not strictly necessary to guard \mzninline{int_uniform}
against being invoked before \mzninline{status()!=START}, since the
\mzninline{sol} constraints will simply not propagate anything in case no
solution has been recorded yet, but we use this simple example to illustrate
how these Boolean conditions are compiled and evaluated.