This tutorial only covers the specifics of the SCS Python interface. For background on SCS and descriptions of the optimization problem being solved and the input data format, please see the SCS README
or the SCS Paper.
This tutorial covers most (but not all) of the Python interface. For more details, see the SCS_Python README
.
In [1]:
from __future__ import print_function
import scs
We'll load data for an example $\ell_1$ minimization problem. data
and cone
are correctly-formatted input for the SCS solver.
In [2]:
m = 2000
data, cone = scs.examples.l1(m=m)
print('data: ', data)
print('cone: ', cone)
scs.solve()
to compute a solution to the problemdata
and cone
are required argumentssol
is a dictionary with keys 'x'
, 'y'
, 's'
(numpy
arrays corresponding to problem variables), and 'info'
(a dict
giving solver status information)
In [3]:
%%time
sol = scs.solve(data, cone, verbose=False)
print('---###---')
In [4]:
print(sol.keys())
sol['info']
Out[4]:
A copy of the SCS default solver settings can be seen by calling scs.default_settings()
. Each of these settings can be modified for a call to scs.solve()
by passing in keyword arguments.
In [5]:
scs.default_settings()
Out[5]:
The solver can be warm-started with vectors x
, y
, and s
(which are expected to be close to the solution). This can reduce the number of iterations needed to converge.
Pass a dictionary with keys 'x'
, 'y'
, and 's'
(pointing to numpy
arrays of the appropriate size) to the warm_start
keyword argument of scs.solve()
. Note that all three vectors are required for warm-starting.
The 'x'
, 'y'
, and 's'
arrays are copied so that they are not modified by SCS. The solution, sol
, returned by scs.solve()
contains new numpy
arrays.
Below, we use the previously-computed solution to warm-start the solver on the same problem, and see that this allows the solver to exit after 0 iterations.
In [6]:
%%time
sol = scs.solve(data, cone, warm_start=sol, verbose=True)
print('---###---')
We can confirm the number of iterations by inspecting the 'info'
dictionary. Note that even though
the solver needed 0 iterations to converge, it still had to perform a matrix factorization (since use_indirect=False
), which took about 4 seconds.
In [7]:
sol['info']
Out[7]:
When using the direct method (use_indirect=False
), we can cache the matrix factorization involving A
, and reuse it across several solves. This is useful when solving a sequence or family of problems where A
is fixed, but b
and c
may change.
The scs.Workspace
object caches the matrix factorization for us, and allows us to call the solver many times with different values for b
and c
. We can also optionally warm-start the solver, and change some of the solver settings between solves.
Below, we initialize the Workspace
object with the same data as above, and note that the setup time (factorization time) is still approximately 4 seconds. Note that the Workspace
defaults to the direct (factorization) method because use_indirect=False
, unless the user specifies otherwise.
In [8]:
%%time
work = scs.Workspace(data, cone)
print('---###---')
In [9]:
work.info
Out[9]:
work.settings
The Workspace
object records changes to the user-specified settings. They can be inspected and modified through the
work.settings
dictionary. When the solver is run, it will operate based on the current settings
.
Some settings should not be changed once the Workspace
object is initialized, since the cached matrix factorization depends on them:
use_indirect
normalize
scale
rho_x
SCS will raise an exception when calling work.solve()
if these have been changed.
In [10]:
work.settings
Out[10]:
In [11]:
work.fixed
Out[11]:
work.data
The vectors b
and c
can be modified through the work.data
dictionary between solves. work.data
is a shallow copy of the data
dictionary passed to the Workspace()
constructor, so changes to work.data
will not affect the original dictionary. However, both dictionaries initially point to the same b
and c
numpy arrays.
Since the cached matrix factorization depends on the original A
, and this matrix cannot be modified without invalidating the factorization,
work.data
does not expose the internally copied matrix A
. Since a copy of A
is formed and stored in the work
object, the original A
matrix can be modified without affecting work
.
In [12]:
work.data
Out[12]:
In [13]:
%%time
sol = work.solve()
print('---###---')
The solution is returned as a dictionary, just as with scs.solve()
.
In [14]:
sol
Out[14]:
settings
By default, the Workspace
will use the settings from scs.default_settings()
. The user has a few opportunities to modify them:
scs.Workspace()
work.settings
(but making sure not to modify the settings in work.fixed
)work.solve()
Any changes to the settings
persist in the Workspace
object, including those passed to scs.solve()
. For instance,
work.solve(eps=1e-5, alpha=1.1)
is exactly equivalent to
work.settings['eps'] = 1e-5
work.settings['alpha'] = 1.1
work.solve()
In addition to benefiting from the cached matrix factorization, we can also use a warm-started solution by calling
work.solve(warm_start=ws)
. This will warm-start from the vectors in ws
. Since we previously ran the solver and the solution is already contained in sol
, the following call should require 0 iterations. It should also require 0 setup time, since we've cached the factorization.
In [15]:
%%time
sol = work.solve(warm_start=sol)
print('---###---')
For a more interesting example of warm-starting, we perturb the b
vector a bit and try to re-solve.
However, if we warm-start from the previous solution to the unperturbed problem, we can expect to only need a few iterations to "correct" for the perturbation and obtain the new solution.
In [16]:
%%time
# make a small perturbation to b
work.data['b'][:m] += .01
sol = work.solve(warm_start=sol)
print('---###---')
If we attempt to solve the problem without warm-starting, it will require many more iterations (and a longer solve time).
In [17]:
%%time
sol = work.solve()
print('---###---')
If we revert even futher (back to the point where we started this tutorial) and try to solve without factorization caching or warm-starting, we can expect an even longer solve time.
In [18]:
%%time
data['b'] = work.data['b']
sol = scs.solve(data, cone)
print('---###---')
In [ ]: