SCS Python Interface Tutorial

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.

Basic SCS Interface


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)


data:  {'A': <10000x8000 sparse matrix of type '<type 'numpy.float64'>'
	with 816000 stored elements in Compressed Sparse Column format>, 'c': array([ 0.,  0.,  0., ...,  1.,  1.,  1.]), 'b': array([-1.06370562,  1.2659559 ,  0.0602342 , ...,  0.        ,
        0.        ,  0.        ])}
cone:  {'l': 8000, 'f': 2000}
  • Use scs.solve() to compute a solution to the problem
  • data and cone are required arguments
  • Solver settings can be passed as keyword arguments
  • sol 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('---###---')


---###---
CPU times: user 9.22 s, sys: 102 ms, total: 9.32 s
Wall time: 9.33 s

In [4]:
print(sol.keys())
sol['info']


['y', 'x', 's', 'info']
Out[4]:
{'dobj': 236.77576439435487,
 'iter': 540,
 'pobj': 236.77653707116332,
 'relGap': 1.6282226554716905e-06,
 'resDual': 0.000986217723616638,
 'resInfeas': 11.836023568070756,
 'resPri': 0.0006949464383811683,
 'resUnbdd': nan,
 'setupTime': 3949.537639,
 'solveTime': 5369.40837,
 'status': u'Solved',
 'statusVal': 1}

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]:
{'alpha': 1.5,
 'cg_rate': 2.0,
 'eps': 0.001,
 'max_iters': 2500,
 'normalize': True,
 'rho_x': 0.001,
 'scale': 1.0,
 'use_indirect': False,
 'verbose': True}

Warm-Starting

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('---###---')


----------------------------------------------------------------------------
	SCS v1.2.3 - Splitting Conic Solver
	(c) Brendan O'Donoghue, Stanford University, 2012-2016
----------------------------------------------------------------------------
Lin-sys: sparse-direct, nnz in A = 816000
eps = 1.00e-03, alpha = 1.50, max_iters = 2500, normalize = 1, scale = 1.00
Variables n = 8000, constraints m = 10000
Cones:	primal zero / dual free vars: 2000
	linear vars: 8000
Setup time: 3.88e+00s
SCS using variable warm-starting
----------------------------------------------------------------------------
 Iter | pri res | dua res | rel gap | pri obj | dua obj | kap/tau | time (s)
----------------------------------------------------------------------------
     0| 6.91e-04  9.85e-04  1.65e-06  2.37e+02  2.37e+02  0.00e+00  2.50e-02 
----------------------------------------------------------------------------
Status: Solved
Timing: Solve time: 2.52e-02s
	Lin-sys: nnz in L factor: 2837000, avg solve time: 1.90e-02s
	Cones: avg projection time: 3.24e-05s
----------------------------------------------------------------------------
Error metrics:
dist(s, K) = 5.6957e-16, dist(y, K*) = 0.0000e+00, s'y/|s||y| = -3.8364e-18
|Ax + s - b|_2 / (1 + |b|_2) = 6.9090e-04
|A'y + c|_2 / (1 + |c|_2) = 9.8547e-04
|c'x + b'y| / (1 + |c'x| + |b'y|) = 1.6513e-06
----------------------------------------------------------------------------
c'x = 236.7751, -b'y = 236.7744
============================================================================
---###---
CPU times: user 3.86 s, sys: 47.1 ms, total: 3.91 s
Wall time: 3.92 s

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]:
{'dobj': 236.77435833406983,
 'iter': 0,
 'pobj': 236.77514193941622,
 'relGap': 1.6512615563459192e-06,
 'resDual': 0.000985465476626838,
 'resInfeas': 11.836107050503301,
 'resPri': 0.0006909028315568697,
 'resUnbdd': nan,
 'setupTime': 3877.593222,
 'solveTime': 25.177226,
 'status': u'Solved',
 'statusVal': 1}

Factorization Caching Interface

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('---###---')


----------------------------------------------------------------------------
	SCS v1.2.3 - Splitting Conic Solver
	(c) Brendan O'Donoghue, Stanford University, 2012-2016
----------------------------------------------------------------------------
Lin-sys: sparse-direct, nnz in A = 816000
eps = 1.00e-03, alpha = 1.50, max_iters = 2500, normalize = 1, scale = 1.00
Variables n = 8000, constraints m = 10000
Cones:	primal zero / dual free vars: 2000
	linear vars: 8000
Setup time: 4.33e+00s
---###---
CPU times: user 4.18 s, sys: 73.9 ms, total: 4.25 s
Wall time: 4.33 s

work.info

work.info will give a copy of the info dictionary, showing solver status information. Since only the setup has run, only the setupTime key is nonzero.


In [9]:
work.info


Out[9]:
{'dobj': 0.0,
 'iter': 0,
 'pobj': 0.0,
 'relGap': 0.0,
 'resDual': 0.0,
 'resInfeas': 0.0,
 'resPri': 0.0,
 'resUnbdd': 0.0,
 'setupTime': 4329.3424,
 'solveTime': 0.0,
 'status': u'',
 'statusVal': 0}

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]:
{'alpha': 1.5,
 'cg_rate': 2.0,
 'eps': 0.001,
 'max_iters': 2500,
 'normalize': True,
 'rho_x': 0.001,
 'scale': 1.0,
 'use_indirect': False,
 'verbose': True}

work.fixed

The user can see a copy of the settings fixed at Workspace initialization time with the work.fixed attribute. The Workspace will raise an exception if these values are changed in settings.


In [11]:
work.fixed


Out[11]:
{'normalize': True, 'rho_x': 0.001, 'scale': 1.0, 'use_indirect': False}

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]:
{'b': array([-1.06370562,  1.2659559 ,  0.0602342 , ...,  0.        ,
         0.        ,  0.        ]),
 'c': array([ 0.,  0.,  0., ...,  1.,  1.,  1.])}

work.solve()

work.solve() will call the solver based on the initial factorizatoin, current settings, (optional) warm-start vectors, data['b'] and data['c'].

Calling the solver below, note that we skip the setup step, and go right to the iterative procedure.


In [13]:
%%time
sol = work.solve()
print('---###---')


SCS using variable warm-starting
----------------------------------------------------------------------------
 Iter | pri res | dua res | rel gap | pri obj | dua obj | kap/tau | time (s)
----------------------------------------------------------------------------
     0| 7.71e-01  3.77e+00  9.99e-01 -6.95e+02  7.56e+02  0.00e+00  3.27e-02 
   100| 8.01e-03  1.05e-02  3.24e-05  2.37e+02  2.37e+02  1.14e-14  1.13e+00 
   200| 3.13e-03  4.04e-03  9.09e-06  2.37e+02  2.37e+02  1.14e-14  2.13e+00 
   300| 1.76e-03  2.31e-03  2.85e-06  2.37e+02  2.37e+02  1.14e-14  3.11e+00 
   400| 1.12e-03  1.52e-03  2.79e-06  2.37e+02  2.37e+02  1.14e-14  4.10e+00 
   500| 7.84e-04  1.09e-03  2.96e-07  2.37e+02  2.37e+02  3.43e-14  5.11e+00 
   540| 6.95e-04  9.86e-04  1.63e-06  2.37e+02  2.37e+02  1.14e-14  5.48e+00 
----------------------------------------------------------------------------
Status: Solved
Timing: Solve time: 5.48e+00s
	Lin-sys: nnz in L factor: 2837000, avg solve time: 9.77e-03s
	Cones: avg projection time: 3.12e-05s
----------------------------------------------------------------------------
Error metrics:
dist(s, K) = 4.2758e-16, dist(y, K*) = 0.0000e+00, s'y/|s||y| = -2.1886e-18
|Ax + s - b|_2 / (1 + |b|_2) = 6.9495e-04
|A'y + c|_2 / (1 + |c|_2) = 9.8622e-04
|c'x + b'y| / (1 + |c'x| + |b'y|) = 1.6282e-06
----------------------------------------------------------------------------
c'x = 236.7765, -b'y = 236.7758
============================================================================
---###---
CPU times: user 5.45 s, sys: 21.1 ms, total: 5.47 s
Wall time: 5.49 s

The solution is returned as a dictionary, just as with scs.solve().


In [14]:
sol


Out[14]:
{'info': {'dobj': 236.77576439435487,
  'iter': 540,
  'pobj': 236.77653707116332,
  'relGap': 1.6282226554716905e-06,
  'resDual': 0.000986217723616638,
  'resInfeas': 11.836023568070756,
  'resPri': 0.0006949464383811683,
  'resUnbdd': nan,
  'setupTime': 4329.3424,
  'solveTime': 5482.592178,
  'status': u'Solved',
  'statusVal': 1},
 's': array([  5.76894725e-17,  -1.18257217e-16,   1.11692482e-16, ...,
         -7.13773240e-18,   1.07065986e-17,   2.41523846e-01]),
 'x': array([  6.95955084e-05,   3.37737847e-05,  -4.60801796e-06, ...,
         -8.98092290e-08,  -8.98092290e-08,   1.20765750e-01]),
 'y': array([ 0.19976614, -0.21751859,  0.24312801, ...,  0.89716835,
         0.78925212,  0.        ])}

Workspace settings

By default, the Workspace will use the settings from scs.default_settings(). The user has a few opportunities to modify them:

  • at initialization time, by passing keyword arguments to scs.Workspace()
  • between solves, by modifying work.settings (but making sure not to modify the settings in work.fixed)
  • just before solve time, by passing keyword arguments to 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()

Caching and warm-starting

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('---###---')


SCS using variable warm-starting
----------------------------------------------------------------------------
 Iter | pri res | dua res | rel gap | pri obj | dua obj | kap/tau | time (s)
----------------------------------------------------------------------------
     0| 6.91e-04  9.85e-04  1.65e-06  2.37e+02  2.37e+02  0.00e+00  3.68e-02 
----------------------------------------------------------------------------
Status: Solved
Timing: Solve time: 3.70e-02s
	Lin-sys: nnz in L factor: 2837000, avg solve time: 3.01e-02s
	Cones: avg projection time: 9.92e-05s
----------------------------------------------------------------------------
Error metrics:
dist(s, K) = 5.6957e-16, dist(y, K*) = 0.0000e+00, s'y/|s||y| = -3.8364e-18
|Ax + s - b|_2 / (1 + |b|_2) = 6.9090e-04
|A'y + c|_2 / (1 + |c|_2) = 9.8547e-04
|c'x + b'y| / (1 + |c'x| + |b'y|) = 1.6513e-06
----------------------------------------------------------------------------
c'x = 236.7751, -b'y = 236.7744
============================================================================
---###---
CPU times: user 37.8 ms, sys: 2.64 ms, total: 40.4 ms
Wall time: 41.1 ms

Perturbed warm-starting

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('---###---')


SCS using variable warm-starting
----------------------------------------------------------------------------
 Iter | pri res | dua res | rel gap | pri obj | dua obj | kap/tau | time (s)
----------------------------------------------------------------------------
     0| 7.68e-04  1.22e-03  3.29e-06  2.37e+02  2.37e+02  0.00e+00  3.47e-02 
    20| 6.94e-04  9.88e-04  4.63e-06  2.37e+02  2.37e+02  3.02e-14  2.76e-01 
----------------------------------------------------------------------------
Status: Solved
Timing: Solve time: 2.76e-01s
	Lin-sys: nnz in L factor: 2837000, avg solve time: 1.24e-02s
	Cones: avg projection time: 3.76e-05s
----------------------------------------------------------------------------
Error metrics:
dist(s, K) = 5.5911e-16, dist(y, K*) = 0.0000e+00, s'y/|s||y| = 1.3401e-18
|Ax + s - b|_2 / (1 + |b|_2) = 6.9441e-04
|A'y + c|_2 / (1 + |c|_2) = 9.8762e-04
|c'x + b'y| / (1 + |c'x| + |b'y|) = 4.6340e-06
----------------------------------------------------------------------------
c'x = 236.7743, -b'y = 236.7721
============================================================================
---###---
CPU times: user 273 ms, sys: 3.67 ms, total: 277 ms
Wall time: 279 ms

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('---###---')


SCS using variable warm-starting
----------------------------------------------------------------------------
 Iter | pri res | dua res | rel gap | pri obj | dua obj | kap/tau | time (s)
----------------------------------------------------------------------------
     0| 7.73e-01  3.76e+00  9.99e-01 -6.95e+02  7.55e+02  0.00e+00  3.45e-02 
   100| 8.01e-03  1.05e-02  3.01e-05  2.37e+02  2.37e+02  2.28e-14  1.12e+00 
   200| 3.14e-03  4.03e-03  1.20e-05  2.37e+02  2.37e+02  2.28e-14  2.18e+00 
   300| 1.77e-03  2.30e-03  2.63e-06  2.37e+02  2.37e+02  2.29e-14  3.20e+00 
   400| 1.12e-03  1.52e-03  1.37e-06  2.37e+02  2.37e+02  2.29e-14  4.24e+00 
   500| 7.52e-04  1.12e-03  6.14e-07  2.37e+02  2.37e+02  2.29e-14  5.32e+00 
   540| 7.01e-04  9.66e-04  1.45e-06  2.37e+02  2.37e+02  2.29e-14  5.75e+00 
----------------------------------------------------------------------------
Status: Solved
Timing: Solve time: 5.75e+00s
	Lin-sys: nnz in L factor: 2837000, avg solve time: 1.02e-02s
	Cones: avg projection time: 3.23e-05s
----------------------------------------------------------------------------
Error metrics:
dist(s, K) = 7.1686e-16, dist(y, K*) = 0.0000e+00, s'y/|s||y| = -4.2676e-19
|Ax + s - b|_2 / (1 + |b|_2) = 7.0129e-04
|A'y + c|_2 / (1 + |c|_2) = 9.6643e-04
|c'x + b'y| / (1 + |c'x| + |b'y|) = 1.4495e-06
----------------------------------------------------------------------------
c'x = 236.7755, -b'y = 236.7748
============================================================================
---###---
CPU times: user 5.71 s, sys: 24.9 ms, total: 5.73 s
Wall time: 5.75 s

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('---###---')


----------------------------------------------------------------------------
	SCS v1.2.3 - Splitting Conic Solver
	(c) Brendan O'Donoghue, Stanford University, 2012-2016
----------------------------------------------------------------------------
Lin-sys: sparse-direct, nnz in A = 816000
eps = 1.00e-03, alpha = 1.50, max_iters = 2500, normalize = 1, scale = 1.00
Variables n = 8000, constraints m = 10000
Cones:	primal zero / dual free vars: 2000
	linear vars: 8000
Setup time: 4.04e+00s
SCS using variable warm-starting
----------------------------------------------------------------------------
 Iter | pri res | dua res | rel gap | pri obj | dua obj | kap/tau | time (s)
----------------------------------------------------------------------------
     0| 7.73e-01  3.76e+00  9.99e-01 -6.95e+02  7.55e+02  0.00e+00  2.78e-02 
   100| 8.01e-03  1.05e-02  3.01e-05  2.37e+02  2.37e+02  2.28e-14  1.04e+00 
   200| 3.14e-03  4.03e-03  1.20e-05  2.37e+02  2.37e+02  2.28e-14  2.02e+00 
   300| 1.77e-03  2.30e-03  2.63e-06  2.37e+02  2.37e+02  2.29e-14  3.08e+00 
   400| 1.12e-03  1.52e-03  1.37e-06  2.37e+02  2.37e+02  2.29e-14  4.15e+00 
   500| 7.52e-04  1.12e-03  6.14e-07  2.37e+02  2.37e+02  2.29e-14  5.88e+00 
   540| 7.01e-04  9.66e-04  1.45e-06  2.37e+02  2.37e+02  2.29e-14  6.45e+00 
----------------------------------------------------------------------------
Status: Solved
Timing: Solve time: 6.45e+00s
	Lin-sys: nnz in L factor: 2837000, avg solve time: 1.15e-02s
	Cones: avg projection time: 3.59e-05s
----------------------------------------------------------------------------
Error metrics:
dist(s, K) = 7.1686e-16, dist(y, K*) = 0.0000e+00, s'y/|s||y| = -4.2676e-19
|Ax + s - b|_2 / (1 + |b|_2) = 7.0129e-04
|A'y + c|_2 / (1 + |c|_2) = 9.6643e-04
|c'x + b'y| / (1 + |c'x| + |b'y|) = 1.4495e-06
----------------------------------------------------------------------------
c'x = 236.7755, -b'y = 236.7748
============================================================================
---###---
CPU times: user 10.1 s, sys: 162 ms, total: 10.2 s
Wall time: 10.5 s

In [ ]: