Scipy

Scipy is a python library for scientific computing. It contains modules for linear algebra, root finding, optimization, etc.This tutorial will cover basic uses of scipy for:

  • Linear Algebra
  • Root finding and Optimization

Linear Algebra

The scipy.linalg module contains several useful linear algebra operation. The example below shows how you can use scipy to invert a matrix.

In [1]:
import numpy as np
import scipy.linalg as sp
a = np.array([[1,2],[3,4]])
ainv = sp.inv(a)
print("ainv = ", ainv)
print(a.dot(ainv))
ainv =  [[-2.   1. ]
 [ 1.5 -0.5]]
[[  1.00000000e+00   0.00000000e+00]
 [  8.88178420e-16   1.00000000e+00]]

If you want to find the determinant, then use the det() function.

In [2]:
print(sp.det(a))
-2.0

If you want to solve, a systems of linear equations, use the solve function.

$$ x + 2y = 10 \\ 3x + 4y = 20 $$

In [3]:
b = np.array([[10,20]])
sp.solve(a, b.T)
Out[3]:
array([[ 0.],
       [ 5.]])

Root finding and Optimization

Scipy has implemented several root finding and optimization algorithms. This website contains all the optimization and root finding methods implemented in scipy and their syntaxes.

For example if want to find the root of $x^3 - 10$ using bisection.

In [4]:
import numpy as np
import scipy.optimize as sp
g = lambda x: (x**3) - 10
x = sp.bisect(g, 1.5, 3.0)
print(x)
2.154434690031394

If you change the convergence criterion.

In [5]:
# Changing convergence criterion
x = sp.bisect(g, 1.5, 3.0, xtol = 0.1)
print(x)
2.15625

Scipy also contains other root finding techniques such as false position, Brent, and Newton's method.

Scipy has several optimization routines also. Let us say you want to minimize $f(x) = (x-1)$^2 + e^x$ in the interval [0,2] using Golden Section Method. This can be done using the minimize_scalar() function.

In [6]:
import scipy.optimize as sp
import numpy as np
g = lambda x: (x-1)**2 + np.exp(x) 
res = sp.minimize_scalar(g, bracket=(-1,1,2), method='Golden')
print(res.x)
0.3149230505107306

According to the help pages, "bracket defines the bracketing interval and can either have three items (a, b, c) so that a < b < c and fun(b) < fun(a), fun(c)". So pick the bracket interval carefully. It is also relatively easy to code some of the popular optimization algorithms in Python. For example, the golden section method can be coded as shown below.

In [7]:
import numpy as np
g = lambda x: (x-1)**2 + np.exp(x) 
def goldenopt(f,x_l, x_u, tol, max_iter):
    iter_ctr = 0
    while (x_u-x_l)>tol and iter_ctr<max_iter:
        x_r1 = x_u - 0.618*(x_u - x_l)
        x_r2 = x_l + 0.618*(x_u - x_l)
        print(iter_ctr, x_l, x_r1, x_r2, x_u)
        if f(x_r1) <= f(x_r2):
            x_u = x_r2
            x_r2 = x_r1
            x_r1 = x_u - 0.618*(x_u - x_l)
        else:
            x_l = x_r1
            x_r1 = x_r2
            x_r2  = x_l + 0.618*(x_u - x_l)
        iter_ctr = iter_ctr + 1
    return (x_r1 + x_r2)/2.0, iter_ctr
    
x_l_init = 0
x_u_init = 2.0
tolerance = 0.0001
max_iter = 100
x_f, iter_ctr = goldenopt(g, x_l_init, x_u_init, tolerance, max_iter)
print("Final Solution is: ", x_f)
print("Iteration counter: ", iter_ctr)
0 0 0.764 1.236 2.0
1 0 0.472152 0.763848 1.236
2 0 0.29178993599999997 0.472058064 0.763848
3 0 0.18032618044800003 0.29173188355199997 0.472058064
4 0.18032618044800003 0.29176775996486404 0.360616484483136 0.472058064
5 0.18032618044800003 0.24919707658942197 0.29174558834171405 0.360616484483136
6 0.24919707658942197 0.29175929040482074 0.3180542706677372 0.360616484483136
7 0.29175929040482074 0.31806273854273714 0.3343130363452196 0.360616484483136
8 0.29175929040482074 0.3080148213540531 0.3180575053959872 0.3343130363452196
9 0.3080148213540531 0.3180607394806787 0.324267118218594 0.3343130363452196
10 0.3080148213540531 0.3142231987563077 0.3180587408163394 0.324267118218594
11 0.3080148213540531 0.3118515985886465 0.31422196358174603 0.3180587408163394
12 0.3118515985886465 0.31422272691962516 0.3156876124853607 0.3180587408163394
13 0.3118515985886465 0.31331695589719133 0.31422225517681585 0.3156876124853607
14 0.31331695589719133 0.314222546713872 0.31478202166868 0.3156876124853607
15 0.314222546713872 0.3147822018385807 0.315127957360652 0.3156876124853607
16 0.314222546713872 0.31456841358094195 0.3147820904935821 0.315127957360652
17 0.31456841358094195 0.3147821593047912 0.3149142116368028 0.315127957360652
18 0.3147821593047912 0.31491425416213004 0.3149958625033132 0.315127957360652
19 0.3147821593047912 0.3148637939266266 0.31491422788147777 0.3149958625033132
20 0.3148637939266266 0.3149142441229209 0.3149454123070189 0.3149958625033132
Final Solution is:  0.31490460813542864
Iteration counter:  21

Similarly, the bisection can be coded as shown below. There are plenty of examples on the web of other good optimization codes.

In [8]:
import numpy as np
g = lambda x: (x-1)**2 + np.exp(x) 
g1 = lambda x: 2*(x-1) + np.exp(x)

def bisectopt(f,x_l, x_u, tol, max_iter):
    iter_ctr = 0
    while (x_u-x_l)>tol and iter_ctr<max_iter:
        x_r = (x_l + x_u)/2.0
        print(iter_ctr, x_l, x_u, x_r)
        if f(x_r) == 0:
            return x_r, iter_ctr
        elif f(x_r) > 0:
            x_u = x_r
        else:
            x_l = x_r
        iter_ctr = iter_ctr + 1
    return x_r,iter_ctr
    
x_l_init = 0
x_u_init = 2.0
tolerance = 0.0001
max_iter = 100
x_f, iter_ctr = bisectopt(g1, x_l_init, x_u_init, tolerance, max_iter)
print("Final Solution is: ", x_f)
print("Iteration counter: ", iter_ctr)
0 0 2.0 1.0
1 0 1.0 0.5
2 0 0.5 0.25
3 0.25 0.5 0.375
4 0.25 0.375 0.3125
5 0.3125 0.375 0.34375
6 0.3125 0.34375 0.328125
7 0.3125 0.328125 0.3203125
8 0.3125 0.3203125 0.31640625
9 0.3125 0.31640625 0.314453125
10 0.314453125 0.31640625 0.3154296875
11 0.314453125 0.3154296875 0.31494140625
12 0.314453125 0.31494140625 0.314697265625
13 0.314697265625 0.31494140625 0.3148193359375
14 0.3148193359375 0.31494140625 0.31488037109375
Final Solution is:  0.31488037109375
Iteration counter:  15

Some of the gradient based methods such as quasi newton methods or newton conjugate are also available in python through the minimize() function. For example, the quasi newton method for minimizing $f(x,y) = (x-1)^4 + 5(y-1)^4 + xy$ using quasi newton is shown below.

In [9]:
import scipy.optimize as sp
import numpy as np

def g(x):
    return (x[0]-1)**4 + 5*(x[1]-2)**4 + x[0]*x[1] 

x_init = np.array([4.0,4.0])
res = sp.minimize(g, x_init, method='BFGS')
print(res.x)
[ 0.23771415  1.7717857 ]

If you want to use Conjugate Gradient, then you can just change the method parameter.

In [10]:
res = sp.minimize(g, x_init, method='CG')
print(res.x)
[ 0.23771511  1.77178729]

Writing your own code for some of these optimization algorithms is not that difficult and should be possible with some practise. For example, the following code implements Newton method for minimizing $f(x)=x^2 - 10x + 25$.

In [11]:
import numpy as np
g = lambda x: x**2 - 10*x + 25
g1 = lambda x: 2*x - 10

def grad_desc(f, f1, x_init, step_size, tol, max_iter):
    x_new = x_init
    x_old = 0
    iter_ctr = 0
    while abs(x_new - x_old) > tol and iter_ctr < max_iter:
        x_old = x_new
        gradient = f1(x_old) 
        move = gradient * step_size
        x_new = x_old - move
        iter_ctr = iter_ctr + 1
        print(iter_ctr, x_new, f(x_new))
    return x_new, iter_ctr

x_init = 15.0
step_size = 0.1
tolerance = 0.01
max_iter = 100

x_f, iter_ctr = grad_desc(g, g1, x_init, step_size, tolerance, max_iter)

print("Final Solution is: ", x_f)
print("Iteration counter: ", iter_ctr)
1 13.0 64.0
2 11.4 40.96000000000001
3 10.120000000000001 26.214399999999998
4 9.096 16.777215999999996
5 8.2768 10.737418239999997
6 7.62144 6.871947673599998
7 7.0971519999999995 4.398046511103992
8 6.6777216 2.814749767106562
9 6.34217728 1.8014398509481921
10 6.073741824 1.1529215046068515
11 5.8589934592 0.7378697629483781
12 5.687194767359999 0.472236648286966
13 5.549755813888 0.3022314549036622
14 5.4398046511104 0.19342813113834012
15 5.35184372088832 0.12379400392854123
16 5.281474976710657 0.07922816251426923
17 5.225179981368525 0.05070602400912705
18 5.18014398509482 0.032451855365845717
19 5.144115188075856 0.020769187434137137
20 5.115292150460685 0.013292279957848763
21 5.092233720368548 0.008507059173023634
22 5.073786976294839 0.0054445178707318576
23 5.059029581035871 0.003484491437266257
24 5.047223664828697 0.0022300745198542415
25 5.037778931862958 0.001427247692703304
Final Solution is:  5.037778931862958
Iteration counter:  25

The following implements multi-dimensional gradient descent for minimizing $f(x,y) = (x-1)^4 + 5(y-1)^4 + xy$.

In [12]:
import numpy as np

def g(x):
    return (x[0]-1)**4 + 5*(x[1]-2)**4 + x[0]*x[1] 

def g1(x):
    grad = np.array([0.0,0.0])
    grad[0] = 4*(x[0]-1)**3 + x[1]
    grad[1] = 20*(x[1]-2)**3 + x[0]
    return grad

def back_track_ls(x):
    eta = 1.0
    beta = 0.1
    gamma = 0.1
    while g(x - eta*g1(x)) > g(x) - gamma*eta*np.dot(g1(x),g1(x)):
        eta = beta*eta
    return eta

def grad_desc_2D(f, f1, x_init, step_size, tol, max_iter):
    x_new = np.array([0.0,0.0])
    x_old = np.array([0.0,0.0])
    gradient = np.array([10.0,10.0])
    move = np.array([0.0,0.0])
    x_new = x_init
    iter_ctr = 0
    while np.linalg.norm(x_new - x_old) > tol and iter_ctr < max_iter:
        x_old = x_new
        gradient = f1(x_old) 
        step_size = back_track_ls(x_old)
        move = gradient * step_size
        x_new = x_old - move
        iter_ctr = iter_ctr + 1
    return x_new, iter_ctr

x_init = np.array([4.0,4.0])
x_f = np.array([0.0,0.0])
step_size = 0.0025
tolerance = 0.01
max_iter = 100

x_f, iter_ctr = grad_desc_2D(g, g1, x_init, step_size, tolerance, max_iter)

print("Final Solution is: ", x_f)
print("Functional Evaluation is: ", g(x_f))
print("Iteration counter: ", iter_ctr)
Final Solution is:  [ 0.23622452  1.774477  ]
Functional Evaluation is:  0.772409696452
Iteration counter:  5

The following implements multi-dimensional Newton for the same minimization problem.

In [13]:
import numpy as np

def g(x):
    return (x[0]-1)**4 + 5*(x[1]-2)**4 + x[0]*x[1] 

def g1(x):
    grad = np.array([0.0,0.0])
    grad[0] = 4*(x[0]-1)**3 + x[1]
    grad[1] = 20*(x[1]-2)**3 + x[0]
    return grad

def hessian(x):
    h  =  np.array([[0.0,0.0],[0.0,0.0]])
    h[0,0] = 12*(x[0] - 1)**2
    h[0,1] = 1
    h[1,0] = 1
    h[1,1] = 60*(x[1] - 2)**2
    return h

def newt_2D(f, f1, x_init, step_size, tol, max_iter):
    x_new = np.array([0.0,0.0])
    x_old = np.array([0.0,0.0])
    gradient = np.array([10.0,10.0])
    move = np.array([0.0,0.0])
    x_new = x_init
    iter_ctr = 0
    while np.linalg.norm(x_new - x_old) > tol and iter_ctr < max_iter:
        x_old = x_new
        gradient = f1(x_old) 
        move = step_size*np.dot(np.linalg.inv(hessian(x_old)),gradient)
        x_new = x_old - move
        iter_ctr = iter_ctr + 1
    return x_new, iter_ctr

x_init = np.array([4.0,4.0])
x_f = np.array([0.0,0.0])
step_size = 1.00
tolerance = 0.01
max_iter = 100

x_f, iter_ctr = newt_2D(g, g1, x_init, step_size, tolerance, max_iter)

print("Final Solution is: ", x_f)
print("Functional Evaluation is: ", g(x_f))
print("Iteration counter: ", iter_ctr)
Final Solution is:  [ 0.23771618  1.77178105]
Functional Evaluation is:  0.772394721448
Iteration counter:  17

Linear Programs can be solved using Linprog. The sample code for solving the following LP is shown below:

$$ \text{max}\,\, 90x + 120y \\ 30x + 40y \leq 2000\\ 50x + 20y \leq 2500 \\ 60x + 90y \leq 4000 \\ x,y \geq 0 $$

In [14]:
import numpy as np
import scipy.optimize as sp
Aub = np.array([[30 , 40], [50 , 20] , [60 , 90]])
bub = np.array([2000 , 2500 , 4000])
c = np.array([-90 , -120])
res = sp.linprog(c, A_ub =Aub , b_ub = bub)
if(res. status == 0):
    print("Status : Optimal" )
    print("Optimal value :", res.fun)
    print("X:", res.x)
else :
    print("Optimal solution not found")
Status : Optimal
Optimal value : -5772.72727273
X: [ 43.93939394  15.15151515]

The objective function should be in minimization form. Equality constraints can be modeled by providing another parameter $A_eq$ in the linprog function. The default assumption is non-negativity of all variables. If you want to model variables with specific bounds as shown below.

$$ \text{max}\,\, 10x + 26y \\ x + y \leq 9\\ x \geq 9.5 \\ 0 \leq y \leq 5 $$

In [15]:
Aub = np.array([[1, 1]])
bub = np.array([9])
c = np.array([10, 26])
b = [(9.5, None), (0, 5)]
res = sp.linprog(c, A_ub=Aub, b_ub=bub, bounds = b)
print(res.status)
2

The status of 2 indicates the problem is infeasible.

The minimize function can also be used to solves constrained nonlinear optimization problems. Let us look at solving the following constrained optimization problem.

$$ \text{min} \,\, (x_1 - 1)^2 + (x_2 -2.5)^2 \\ x_1 - 2x_2 + 2 \geq 0 \\ -x_1 - 2x_2 + 6 \geq 0 \\ -x_1 + 2x_2 + 2 \geq 0 \\ x_1, x_2 \geq 0 $$

This is example 16.4 from Jorge Nocedal, Stephen Wright. Numerical Optimization, 1999. This is also the same example solved in the scipy docs available here.

Note that for constrained nonlinear optimization problems:

  • Constraints provided as dictionaries
  • Equality constraints must evaluate to 0
  • Inequality constraints must be nonnegative
  • COBYLA and SLSQP are the two algorithms available
In [16]:
import numpy as np
import scipy.optimize as sp

objfun = lambda x: (x[0] - 1)**2 + (x[1] - 2.5)**2
con1 = lambda x: x[0] - 2 * x[1] + 2
con2 = lambda x: -x[0] - 2 * x[1] + 6
con3 = lambda x: -x[0] + 2 * x[1] + 2
cons = ({'type': 'ineq', 'fun': con1}, {'type': 'ineq', 'fun': con2}, {'type': 'ineq', 'fun': con3})
x_init = (2,0)
res = sp.minimize(objfun, x_init, method='SLSQP', constraints=cons)
print(res.x)
[ 1.4  1.7]

In general for linear and integer programs, I recommend other specialized solvers like CPLEX and Gurobi which all have python interfaces. For other open source solvers which interface with python see here.