Installation

The goal is to have working python with the following packages and bindings:

Ubuntu/Debian Linux

In the following we will explain how to get a native installation on your system. Beside that it is also possible to use the prepared VirtualBox image, follow the Windows/macOS section for this.

Install the packages that are provided by your linux distribution, for Ubuntu up to 16.10:

sudo apt install python-numpy python-scipy python-matplotlib python-pip cython ipython-notebook build-essential libblas-dev liblapack-dev

while for Ubuntu 17.04:

sudo apt install python-numpy python-scipy python-matplotlib python-pip cython jupyter-notebook build-essential libblas-dev liblapack-dev

This gives you numpy, scipy, matplotlib and the ipython notebook respective jupyter notebook.

macOS and Windows

We recommend using Anaconda which bundles all required packages. Installation instructions are provided on the Anaconda website.

Some python snippets

We suggest to start an ipython/jupyter notebook and do some interactive experiments. Execute on the command line (Ubuntu <= 16.10, macOS):

ipython notebook

respective (Ubuntu 17.04)

jupyter notebook

and click on New Notebook.

It is good practice to first include all python packages that are going to be used. We will likely always use numpy that provides some numerical datatypes and standard numerical algorithms as well as the print function from Python 3 replacing the print statement and the / function from Python 3 that automatically type converts integer to floats to avoid counterintuitive behaviour.

After you have entered commands into a cell, press Shift + Enter to execute it.


In [1]:
from __future__ import print_function, division
import numpy as np # numpy will be used a lot, thus it is convenient to address it with np instead of numpy

Primitive Datatypes

Similar to Matlab, python uses dynamical typing with implicit type conversion. Primitive datatypes in python are integers, floating point numbers, boolean and characters. Here are some examples using these primitive and the implicit type conversion:


In [2]:
a = 1           # new integer variable a that is assigned the value 1
b = 2           # new integer variable b that is assigned the value 2
c = 2.0         # new floating point variable c that is assigned the value 2.0
d = True        # new boolean variable d that is assignes the value True

In [3]:
print(b*c)      # automatic type conversion to float


4.0

In [4]:
print(a/b)      # what you expect with automatic type conversion, beware: this is different in standard python 2!


0.5

In [5]:
print(a//b)     # integer division


0

Note that all these datatypes are immutable, this will be explained in the functions paragraph.

Composed datatypes: lists and dictionaries

Furthermore there are the composed datatypes list and dict (short for dictionary). A list is a ordered collection of arbitrary python objects, a dictionary a mapping between arbitrary python objects (the object on the source side have to be hashable). In contrast to the previously encountered datatypes, list and dict are mutable, explained in the function section.


In [6]:
a = [1, 2, 14.0, 3]                # declaration of a list, note that members of the collection may have different types
print(a[0], a[2], a[-1])           # indexing is zero based: this gives the first, third and last element of the list
print(len(a))                      # len gives the length of a list


1 14.0 3
4

In [7]:
b = list(range(10))                # range is a function that constructs a generator giving a list with integers
c = list(range(3,7))
d = [x*x for x in b if x % 2 == 0] # list comprehension is a method to construct new lists out of a given one,
                                   # here all even squares < 100
print(b, c, d)


[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [3, 4, 5, 6] [0, 4, 16, 36, 64]

In [8]:
cd = c + d                         # concatenate lists c and d
print(cd)


[3, 4, 5, 6, 0, 4, 16, 36, 64]

In [9]:
print(c)
c.append(5)                        # extend the list c with another element 5
print(c)


[3, 4, 5, 6]
[3, 4, 5, 6, 5]

In [10]:
e = 'hello world'                  # declares a string which is a list of characters
print(e, e[3])


hello world l

In [11]:
f = {                              # declare a dictionary with the keys 'algorithm', 'tolerance', 'suspicious iterates'
    'algorithm': 'downhill',
    'tolerance': 1e-6,
    'suspicious iterates': [3,7,11]
}
print(f['algorithm'], f.keys())   # access mapped elements of dictionary by their key, the function keys gives a list of keys


downhill dict_keys(['algorithm', 'tolerance', 'suspicious iterates'])

Iterators

Lists can be used as iterators, some examples:


In [12]:
for elem in a:
    print(elem, type(elem))


1 <class 'int'>
2 <class 'int'>
14.0 <class 'float'>
3 <class 'int'>

Note that the second line that constitutes the inner block of the for loop has been indented by 4 characters. This is one syntax rule in python, which you will also encounter in if-else and while statements: Blocks are announced by a : and have to be indented. End of indentation indicates end of block. In Java/C++ you would use { to indicate the begin of a block and } to indicate its end.


In [13]:
# less elegant, but same functionality as in previous example
for ii in range(len(a)):
    print(a[ii], type(a[ii]))


1 <class 'int'>
2 <class 'int'>
14.0 <class 'float'>
3 <class 'int'>

In [14]:
for key in f:
    print(key, ':', f[key])


algorithm : downhill
tolerance : 1e-06
suspicious iterates : [3, 7, 11]

In [15]:
# sum all cubes of integers from 0,...,99
cubesum = 0
for ii in range(100):
    cubesum += ii*ii*ii
print(cubesum)


24502500

Booleans

Comparison for equality is done with == in python, for numerical datatypes also <, <=, >, >= exist. Membership in lists and dictionaries can be tested by in:


In [16]:
print(3 == 7)
print(7 < 7)
print(7 <= 7)
print(3 in range(0,9))
print(4 in ['apple', 'pear'])


False
False
True
True
False

Booleans can be composed with and and or and negated with not:


In [17]:
print(cubesum % 100 == 0 and cubesum < 0)
print(cubesum % 100 == 0 or cubesum < 0)
print(not cubesum in ['apple', 'pear'])


False
True
True

Loops

Using a boolean expression it is also possible to write a while loop that continues as long as a condition is satisfied:


In [18]:
a = 2.0
x = 2.0
# Newton iteration to compute x = sqrt(a) with a residual <= 1e-8
while( abs(x*x-a) > 1e-8 ):
    x = .5*(x+a/x)
print(x, x*x-a)


1.4142135623746899 4.510614104447086e-12

Customary with loops are the break and continue statements that exit the loop execution if a certain condition is satisfied or jump to the next loop execution:


In [19]:
a = 2.0
x = 2.0
count = 0
# Newton iteration to compute x = sqrt(a) with a residual that cannot be achieved in double precision
while True:
    if abs(x*x-a) <= 1e-30: # exit loop because precision is reached
        print("Very precise square root computed")
        break
    if count > 100:         # exit loop because iteration limit is reached
        print("Iteration limit exceeded")
        break
    x = .5*(x+a/x)
    count += 1
print(count, x, x*x-a)


Iteration limit exceeded
101 1.414213562373095 -4.440892098500626e-16

In [20]:
# sum all primes from 10,...,99 (this is inefficient!)
primesum = 0
for ii in range(10,100):
    # test if ii is prime
    prime = True
    for jj in range(2,ii):
        if ii % jj == 0:
            prime = False
    # continue with next iteration if ii is not a prime number
    if not prime:
        continue
    primesum += ii
print(primesum)


1043

Functions

There are two different ways to declare functions. With the def statement and as lambda expression:


In [21]:
def newton_squareroot(a, x0):  # declare a function newton_squareroot that takes two arguments a and x0
    """
    Compute square root of a by Newton iteration, with initial guess x0
    """
    x = 1.0
    it = 0
    itmax = 100
    tol = 1e-12
    while (abs(x*x-a) > tol and it < itmax):
        x = .5*(x+a/x)
        it += 1
    return x                   # return the computed square root x

In [22]:
print(newton_squareroot(2.0, 1.0), newton_squareroot(9.0, 1.0))


1.414213562373095 3.0

In [23]:
entropy = lambda x: -x*np.log(x) # declare a function entropy with one argument by a lambda expression
print(entropy(0.2))


0.321887582487

In [24]:
# declare a function density with three arguments by a lambda expression
density = lambda x, m, s: np.exp(-(x-m)*(x-m)/(2.0*s*s))/(np.sqrt(2.0*np.pi)*s)
print(density(0.3,0.1,1.2))


0.327866430085

In functions declared with def it is also possible to have optional arguments. They have to appear after nonoptional arguments:


In [25]:
def newton_squareroot(a, x0 = 1.0, tol = 1e-12, itmax = 100):
    """
    Compute square root of a by Newton iteration, with initial guess x0
    """
    x = x0
    it = 0
    while (abs(x*x-a) > tol and it < itmax):
        x = .5*(x+a/x)
        it += 1
    return x, it                 # return the computed square root x and the number of iterations

In [26]:
print(newton_squareroot(3.4))


(1.8439088914585775, 5)

Now the difference between mutable and immutable can be explained: arguments passed to a function that are mutable can be changed during function execution, while immutable arguments can not:


In [27]:
def double(x):
    x = x + x
def appendzero(x):
    x.append(0)

In [28]:
# for an immutable argument
x = 1.0
double(x) # inside the function x = x+x, so x = 2.0, but as x is immutable the value does not change outside
print(x)


1.0

In [29]:
# for a mutable argument
x = [1]
appendzero(x) # inside the function x will be appended with 0, so x = [1, 0]
print(x)


[1, 0]

Numpy

In scientific computing, it is required to efficiently handle vector and matrix data. This is done by the numpy package, that we have included as np. Different methods to create some vectors:


In [30]:
a = np.array([3.0, 4.2, -1.0]) # converting an python array of floats to a numpy vector
b = np.zeros([4])              # a numpy vector containing 4 zeros
c = 3.0*np.ones([13])          # a numpy vector containing 13 times the entry 3.0
print(a, b, c)


[ 3.   4.2 -1. ] [ 0.  0.  0.  0.] [ 3.  3.  3.  3.  3.  3.  3.  3.  3.  3.  3.  3.  3.]

In [31]:
d = np.zeros_like(a)           # a numpy vector containing as many zeros as a has entries
e = np.ones_like(b)            # a numpy vector containing as many ones  as b has entries
print(d, e)


[ 0.  0.  0.] [ 1.  1.  1.  1.]

In [32]:
f = np.linspace(3.0, 7.0, 101) # a numpy vector with 101 elements, spaced equidistantly over [3.0, 7.0]
print(f)


[ 3.    3.04  3.08  3.12  3.16  3.2   3.24  3.28  3.32  3.36  3.4   3.44
  3.48  3.52  3.56  3.6   3.64  3.68  3.72  3.76  3.8   3.84  3.88  3.92
  3.96  4.    4.04  4.08  4.12  4.16  4.2   4.24  4.28  4.32  4.36  4.4
  4.44  4.48  4.52  4.56  4.6   4.64  4.68  4.72  4.76  4.8   4.84  4.88
  4.92  4.96  5.    5.04  5.08  5.12  5.16  5.2   5.24  5.28  5.32  5.36
  5.4   5.44  5.48  5.52  5.56  5.6   5.64  5.68  5.72  5.76  5.8   5.84
  5.88  5.92  5.96  6.    6.04  6.08  6.12  6.16  6.2   6.24  6.28  6.32
  6.36  6.4   6.44  6.48  6.52  6.56  6.6   6.64  6.68  6.72  6.76  6.8
  6.84  6.88  6.92  6.96  7.  ]

Matrices can be constructed in a similar way:


In [33]:
A = np.array([              # convert a python array of arrays of floats to a numpy matrix
    [3.0, 0.5],
    [3.7, 1.2],
    [5.0, -3.0]
])
print(A)


[[ 3.   0.5]
 [ 3.7  1.2]
 [ 5.  -3. ]]

In [34]:
B = np.diag(a)              # create a diagonal matrix with diagonal given by the array a
print(B)


[[ 3.   0.   0. ]
 [ 0.   4.2  0. ]
 [ 0.   0.  -1. ]]

some operations with matrices and vectors


In [35]:
# get dimensions
print(a.shape)
print(A.shape)


(3,)
(3, 2)

In [36]:
# transposition
print(A.transpose())
print(A.transpose().shape)


[[ 3.   3.7  5. ]
 [ 0.5  1.2 -3. ]]
(2, 3)

In [37]:
# matrix vector product
print(B.dot(a))


[  9.    17.64   1.  ]

In [38]:
# access first row of matrix A:
print(A[0,:])


[ 3.   0.5]

In [39]:
# access second column of matrix A:
print(A[:,1])


[ 0.5  1.2 -3. ]

The previous examples are a special case of so called slicing in numpy to obtain subvectors and submatrices, further examples:


In [40]:
print(f[:4])       # the first four elements of f, equivalent to f[0:4]
print(f[12:18])    # elements f[12], ..., f[17]
print(f[::20])     # every twentieth element of f
print(f[1::20])    # every twentieth element of f, but starting at the second position
print(f[[0,5,8]])  # the first, sixth, ninth element of f
print(f[85:-4])    # all elements of f starting with the 86st and omitting the four last elements


[ 3.    3.04  3.08  3.12]
[ 3.48  3.52  3.56  3.6   3.64  3.68]
[ 3.   3.8  4.6  5.4  6.2  7. ]
[ 3.04  3.84  4.64  5.44  6.24]
[ 3.    3.2   3.32]
[ 6.4   6.44  6.48  6.52  6.56  6.6   6.64  6.68  6.72  6.76  6.8   6.84]

It is sometimes necessary to "vectorize" matrices, forming the vector with all matrix rows appended.


In [41]:
vecA = A.reshape(-1) # vectorize A
print(vecA)
reshapedA = vecA.reshape([3,2])
print(reshapedA)     # change shape of A to 3 x 2 instead 3 x 2, order of elements is preserved


[ 3.   0.5  3.7  1.2  5.  -3. ]
[[ 3.   0.5]
 [ 3.7  1.2]
 [ 5.  -3. ]]

In [42]:
# compute trace of matrix
print(B.trace())


6.2

In [43]:
# compute sum and norms of vector
print(u'\u03a3\u2096a\u2096 =', a.sum())
print(u'\u2016a\u2016\u2082 =', np.linalg.norm(a))
print(u'\u2016a\u2016\u2081 =', np.linalg.norm(a, 1))
print(u'\u2016a\u2016\u221e =', np.linalg.norm(a, np.inf) )


Σₖaₖ = 6.2
‖a‖₂ = 5.25737577124
‖a‖₁ = 8.2
‖a‖∞ = 4.2

In [44]:
# solve linear system (backslash in matlab)
np.linalg.solve(B, np.ones([3])) # find x with B*x = [1,1,1]


Out[44]:
array([ 0.33333333,  0.23809524, -1.        ])

In [45]:
# compute inverse of A (you should always prefer solve or getting a decomposition)
print(np.linalg.inv(B))


[[ 0.33333333  0.          0.        ]
 [ 0.          0.23809524  0.        ]
 [-0.         -0.         -1.        ]]

It is often necessary to construct vectors and matrices from vectors and matrix of smaller dimensions by stacking or forming block matrices. This can be done with the concatenate and bmat in numpy.


In [46]:
ab = np.concatenate([a, b])   # vector obtained by stacking a and b
print(ab)
K = np.bmat([[np.eye(3), A], [A.transpose(), np.zeros([2,2])]]) # block matrix, eye(n): identity in R^n
print(K)


[ 3.   4.2 -1.   0.   0.   0.   0. ]
[[ 1.   0.   0.   3.   0.5]
 [ 0.   1.   0.   3.7  1.2]
 [ 0.   0.   1.   5.  -3. ]
 [ 3.   3.7  5.   0.   0. ]
 [ 0.5  1.2 -3.   0.   0. ]]

Many of the standard functions are available in numpy and vectorized: They can be applied to a numpy vector and return a vector with the function applied to the components. This is very efficiently implemented:


In [47]:
print(np.sin(a))
print(np.array([np.sin(x) for x in a])) # same result, but slower


[ 0.14112001 -0.87157577 -0.84147098]
[ 0.14112001 -0.87157577 -0.84147098]

To verify that the first variant is faster, we use %timeit, that determines the average execution time of a python statement. A command starting with % is a IPython magic command that only works in ipython and ipython notebooks, but not in python scripts.


In [48]:
%timeit test = np.sin(np.linspace(0.0, 1.0, 1000))


The slowest run took 5.08 times longer than the fastest. This could mean that an intermediate result is being cached.
10000 loops, best of 3: 27 µs per loop

In [49]:
%timeit test = np.array([np.sin(ii*1e-3) for ii in range(1000)])


1000 loops, best of 3: 730 µs per loop

To get the execution time of a code block in python, you can use the time.time() of the time module that gives you the current system time:


In [50]:
def fib(n):
    if n in [0, 1]:
        return 1
    return fib(n-1) + fib(n-2)
import time
zerotime = time.time()
print('30th fibonacci number:', fib(30))
executiontime = time.time() - zerotime
print('took {:.2f} s to compute using recursion'.format(executiontime))


30th fibonacci number: 1346269
took 0.36 s to compute using recursion

You can also create a custom vectorized function out of a function that is defined for scalar values:


In [51]:
newton_squareroot_vectorized = np.vectorize(newton_squareroot)

In [52]:
print(newton_squareroot_vectorized(np.linspace(1.0, 10.0, 5)))


(array([ 1.        ,  1.80277564,  2.34520788,  2.78388218,  3.16227766]), array([0, 5, 6, 6, 6]))

In [53]:
%timeit test = newton_squareroot_vectorized(np.linspace(1.0, 10.0, 100))


1000 loops, best of 3: 195 µs per loop

In [54]:
%timeit test = [newton_squareroot_vectorized(x) for x in np.linspace(1.0, 10.0, 100)]


100 loops, best of 3: 2.02 ms per loop

Plotting with Matplotlib

First load the plotting module from matplotlib as plt and activate the notebook plotting extension for ipython notebook (not necessary in scripts).


In [55]:
import matplotlib.pyplot as plt
%matplotlib notebook

In [56]:
xs  = np.linspace(-5.0, 10.0, 1000)
ys  = np.sin(xs)
xs2 = np.linspace(-2.0, 8.0, 25)
ys2 = np.cos(xs2)
plt.figure()
plt.title('A sample plot')
plt.plot(xs,  ys)
plt.plot(xs2, ys2, 'o')
plt.show()



In [57]:
# Compute iterates in Newton iteration for x^2 = 2
iterates = [1.0]
a = 2.0
while abs(iterates[-1]*iterates[-1] - a) > 1e-15:
    iterates.append(.5*(iterates[-1]+a/iterates[-1]))
residuals = [abs(it*it-a) for it in iterates]
plt.figure()
plt.title(r'Convergence Newton Iteration $ x^2 = 2 $')
plt.xlabel(r'iteration $k$')
plt.ylabel(r'residual $\vert x_k^2 - 2 \vert$')
plt.semilogy([0, len(iterates)-1], [1e-15, 1e-15], '--')
plt.semilogy(range(len(iterates)), residuals, '-o')
plt.show()


Solving an initial value problems with scipy.integrate

Initial value problems as considered in the lecture are of the form $$ \dot x(t) = f(t, x(t), p), \quad x(t_0) = x_0 $$ where $ p $ is a parameter.

Scipy provides with ODE a class to solve ordinary differential equations.

The following function provides a convenience interface to solve an initial value problem as specified above:


In [58]:
import scipy.integrate

class IVPResult:
    pass

def solve_ivp(f, ts, x0, p=None, integrator='dopri5', store_trajectory=False):
    """
    Solve initial value problem
        d/dt x = f(t, x, p);  x(t0) = x0.

    Evaluate Solution at time points specified in ts, with ts[0] = t0.
    """

    ivp = scipy.integrate.ode(f)
    ivp.set_integrator(integrator)

    if store_trajectory:
        times = []
        points = []
        def solout(t, x):
            if len(times) == 0 or t != times[-1]:
                times.append(t)
                points.append(np.copy(x[:x0.shape[0]]))
        ivp.set_solout(solout)

    ivp.set_initial_value(x0, ts[0])
    ivp.set_f_params(p)

    result = IVPResult()
    result.ts = ts
    result.xs = np.zeros([ts.shape[0], x0.shape[0]])
    result.success = True

    result.xs[0,:] = x0
    for ii in range(1,ts.shape[0]):
        ivp.integrate(ts[ii])
        result.xs[ii,:] = ivp.y[:x0.shape[0]]
        if not ivp.successful():
            result.success = False
            break

    if store_trajectory:
        result.trajectory_t = np.array(times)
        result.trajectory_x = np.array(points)

    return result

As an example, we consider a harmonic oscillator that is driven by a periodic external force with frequency $ \omega $. The system is described by the differential equation $$ \ddot x + k^2 x = F \sin(\omega t) $$ with $ k = \sqrt{D/m} $ where $ D $ is the spring constant and $ m $ mass, furthermore we set $ F = 1 $.

Reformulation as a system of first order is given by $$ \begin{pmatrix} \dot x_0 \\ \dot x_1 \end{pmatrix} = \begin{pmatrix} x_1 \\ -k^2 x_0 + \sin(\omega t) \end{pmatrix}. $$

We have to define the function $ f $ for the right hand side in python where we set $ p = \begin{pmatrix} k \\ \omega \end{pmatrix} $.


In [59]:
def harm_osc(t, x, p):
    k = p[0]
    omega = p[1]
    return np.array([x[1], -k*k*x[0]+np.sin(omega*t)])

First we use $ k = 1, \omega = 2 $ and solve the initial value problem with $ x(0) = 1, \dot x(0) = 0 $ as initial value on the time horizon $ [0, 25] $ and store the trajectory as computed by the initial value solver:


In [60]:
result = solve_ivp(harm_osc, np.array([0., 25.]), np.array([1., 0.]), p=np.array([1., 2.]), store_trajectory=True)
print(result.xs)
fig = plt.figure()
plt.plot(result.trajectory_t, result.trajectory_x)
plt.show()


[[ 1.          0.        ]
 [ 0.99042604  0.14984178]]

Now the case of resonance disaster: we set $ k = \omega = 1 $:


In [61]:
result = solve_ivp(harm_osc, np.array([0., 25.]), np.array([1., 0.]), p=np.array([1., 1.]), store_trajectory=True)
print(result.xs)
fig = plt.figure()
plt.plot(result.trajectory_t, result.trajectory_x)
plt.show()


[[  1.           0.        ]
 [-11.46499768  -1.52203744]]

Solving an optimization problem using the SQP solver SLSQP provided by scipy

We use the problem HS71 from the Hock-Schittkowski collection of optimization problems. This is given by $$ \begin{array}{ll} \min_{x \in \mathbb R^4} & x_0 x_3 (x_0+x_1+x_2) \\ \text{s.t.} & x_0 x_1 x_2 x_3 -25 \ge 0 \\ & \Vert x \Vert_2^2 - 40 = 0 \\ & 1 \le x_i \le 5 \quad (i=0,\ldots,3) \end{array}. $$

Now we define functions

  • ffcn that computes $ f(x) := x_0 x_3 (x_0+x_1+x_2) + x_2 $,
  • dfcn that computes $ \nabla f(x) = \begin{pmatrix} x_3 (x_0+x_1+x_2) + x_0 x_3 \\ x_0 x_3 \\ x_0 x_3 + 1 \\ x_0 (x_0+x_1+x_2) + x_0 x_3 \end{pmatrix} $
  • cfcn_ieq that computes $ c_I(x) := x_0 x_1 x_2 x_3 - 25 $
  • cfcn_eq that computes $ c_E(x) := \Vert x \Vert_2^2 - 40 $
  • jfcn_ieq that computes $ Jc_I(x) = \nabla c_I(x)^T = \begin{pmatrix} x_1 x_2 x_3 & x_0 x_2 x_3 & x_0 x_1 x_3 & x_0 x_1 x_2 \end{pmatrix}$
  • jfcn_eq that computes $ Jc_E(x) = \nabla c_E(x)^T = \begin{pmatrix} 2 x_0 & 2 x_1 & 2 x_2 & 2 x_3 \end{pmatrix}$

In [62]:
ffcn = lambda x: x[0]*x[3]*np.sum(x[:3]) + x[2]
dfcn = lambda x: np.array([x[3]*np.sum(x[:3])+x[0]*x[3], x[0]*x[3], x[0]*x[3]+1, x[0]*np.sum(x[:3])])
cfcn_ieq = lambda x: np.array([np.prod(x)-25.0])
cfcn_eq = lambda x: np.array([np.inner(x,x)-40])

def jfcn_ieq(x):
    return np.array([
            [np.prod(x[1:]), np.prod(x[[0,2,3]]), np.prod(x[[0,1,3]]), np.prod(x[:3])]
        ])
jfcn_eq = lambda x: np.array([2*x])

For sanity reasons we test first if our implementation has no obvious bugs by comparing with finite differences at a random point.


In [63]:
x0 = np.random.randn(4) # x0 is an array with 4 entries chosen from a normal distribution
h = 1e-8                # perturbation in finite differences
# get unit vectors
unit = np.eye(4)
# compute finite difference approximation to gradient in x0
fx0  = ffcn(x0) # store f(x0) to compute it only once
dfx0 = np.array([ffcn(x0+h*unit[ii,:])-fx0 for ii in range(4)])/h # approximation to gradient
print(np.linalg.norm(dfx0 - dfcn(x0)), np.inf)
# compute finite difference approximation to inequality jacobian in x0
cix0  = cfcn_ieq(x0)
jcix0 = (np.array([cfcn_ieq(x0+h*unit[ii,:])-cix0 for ii in range(4)])/h).transpose()
print(np.linalg.norm(jcix0 - jfcn_ieq(x0)), np.inf)
# compute finite difference approximation to equality jacobian in x0
cex0  = cfcn_eq(x0)
jcex0 = (np.array([cfcn_eq(x0+h*unit[ii,:])-cex0 for ii in range(4)])/h).transpose()
print(np.linalg.norm(jcex0 - jfcn_eq(x0)), np.inf)


2.34944511877e-08 inf
3.60945368208e-07 inf
7.40105126436e-07 inf

The resulting deviations from the finite difference approximations give some confidence that we have done a correct implementation of the gradient and jacobian. Let us now solve the optimization problem with initial guess $ x^0 = \begin{pmatrix} 1 \\ 5 \\ 5 \\ 1 \end{pmatrix} $. Using the specified options shows a one line summary per SQP iteration.


In [64]:
import scipy.optimize

n  = 4                          # number of variables
m  = 2                          # number of constraints
l  = np.ones([n])               # lower bound variables
u  = 5.0*np.ones([n])           # upper bound variables
x0 = np.array([1., 5., 5., 1.]) # initial guess

result = scipy.optimize.minimize(ffcn, x0, method='SLSQP', jac=dfcn,
                        bounds=[(l[i], u[i]) for i in range(l.shape[0])],
                        constraints=[
                            {'type': 'eq', 'fun': cfcn_eq, 'jac': jfcn_eq},
                            {'type': 'ineq', 'fun': cfcn_ieq, 'jac': jfcn_ieq}
                        ],
                        options={'disp': True, 'iprint': 2})


  NIT    FC           OBJFUN            GNORM
    1     1     1.600000E+01     1.643168E+01
    2     2     1.606250E+01     1.680041E+01
    3     3     1.696396E+01     1.759009E+01
    4     4     1.701372E+01     1.764611E+01
    5     5     1.701402E+01     1.764620E+01
Optimization terminated successfully.    (Exit mode 0)
            Current function value: 17.0140172456
            Iterations: 5
            Function evaluations: 5
            Gradient evaluations: 5

result is now a dictionary containing the solution vector and some status information:


In [65]:
print(result)


     fun: 17.014017245572898
     jac: array([ 14.57227022,   1.37940764,   2.37940764,   9.56415073,   0.        ])
 message: 'Optimization terminated successfully.'
    nfev: 5
     nit: 5
    njev: 5
  status: 0
 success: True
       x: array([ 1.        ,  4.74299606,  3.82115467,  1.37940764])

Documentation

First and foremost it is useful to consult the documentation of the packages that are used.

There is a plethora of manuals, tutorials, introductions on the web for python and its usage in scientific computing. For example the recent notebooks by Robert Johannson. Using google is usually a good idea.

If you are fluent in german and looking for another basic introduction, consider the tutorial of the introductory numerical analysis lecture by Andreas Potschka.


In [ ]: