I’ve been planning to write this post for a while, but it was hard to choose
the right solver. Most mainstream solvers already have AMPL interfaces and I din’t
want to reimplement them. At the same time I didn’t want to use an obscure solver
noone heard of.
The new C++ API is a natural object-oriented interface for connecting solvers
to AMPL. It provides classes for working with optimization problems, options and
expression trees. This API allows you to write type-safe and efficient driver code,
eliminates most of the boilerplate and has virtually zero overhead
compared to the C API.
Although the C++ API was initially designed for constraint programming solvers,
it can be used to connect other kinds of solvers as well. It has been used to
connect the following solvers: IBM/ILOG CPLEX CP Optimizer,
Gecode, JaCoP and
In this post I’ll use the C++ ASL API because it simplifies writing a
solver connection. Besides LocalSolver itself only provides C++ and not C API.
To get started, let’s clone the AMPL GitHub repository:
The solvers directory contains AMPL drivers for a number of solvers
which can be used as examples. As I mentioned before,
written using the C++ API.
Let’s create solvers/localsolver directory that will contain the driver code,
localsolver.h, localsolver.cc and main.cc. The first two files will
contain the solver interface and the last one will implement the main function.
Every solver interface should at least
Provide access to solver options
Convert problem from AMPL to native solver format
Solve the problem
Pass solution back to AMPL
LocalSolver provides a nice simple interface, so problem conversion is
pretty straightforward. For the purpose of this post, I’ll only focus on linear
expressions, but converting nonlinear expressions is similar. In fact conversion
of a nonlinear expression is often done in one line of code.
Let’s add class NLToLocalSolverConverter that will convert an AMPL problem
represented by ampl::Problem class to a LocalSolver model localsolver::LSModel:
I use ls as an alias to localsolver and omit the ampl namespace altogether
for brevity. The NLToLocalSolverConverter class contains a reference to
LSModel object and an array of ls::LSExpression* which point to expressions
representing decision variables. Both members are used by conversion methods such
as ConvertExpr The AMPL Problem object is passed to the Convert function
which converts variables, objective and constraints:
The ConvertExpr method takes a linear and nonlinear part of objective or
constraint expression and converts it to a LocalSolver expression:
In the next post I’ll extend this class showing how to use the Visitor design
pattern to traverse nonlinear expression trees.
Now that the conversion is implemented, let’s add a class LocalSolver
representing the solver itself:
This class provides access to solver options (currently only timelimit and built-in
options). The constructor first passes the information about the solver such as its
name and version to the base Solver class. It also provides a text printed before the
option documentation when the user runs the solver with option -=.
Finally, the constructor adds a timelimit integer option with AddIntOption
method passing the description, getter and setter to it:
The DoSolve method uses NLToLocalSolverConverter described above to convert
the problem. Normally the solver options are set directly in the solver object,
but for some reason LocalSolver requires setting options after the model is
constructed, so we set the options in this method. Then we solve the problem,
convert the solution and pass it to AMPL with HandleSolution:
The method also measures time of each step and reports it if the built-in timing
option is set.
Finally we provide two functions, a CreateSolver function that creates a Solver
object and is used for testing and introspection (e.g. to query options
and a main method that constructs and runs the solver:
The easiest way to build an AMPL driver is by using CMake, a cross-platform build
system. All we need for a new driver is to add the following line to
Now we can go to the top level ampl directory and build the code:
which gives us a localsolver binary that can be used with AMPL:
As you can see connecting a solver to AMPL is not difficult. It took little time and only
~200 lines of code to write a fully functional LocalSolver connection with support for
continous variables, binary variables, linear objectives and constraints (including
range and equality constraints), the time limit option and statistics reporting.