In [8]:
import numpy as np
from qutip import *

Introduction

Often one is interested in the output of a given function as a single-parameter is varied. For instance, we can calculate the steady-state response of our system as the driving frequency is varied. In cases such as this, where each iteration is independent of the others, we can speedup the calculation by performing the iterations in parallel. In QuTiP, parallel computations may be performed using the parallel_map function or the parfor (parallel-for-loop) function.

To use the these functions we need to define a function of one or more variables, and the range over which one of these variables are to be evaluated. For example:

Single-Variable Functions


In [6]:
def func1(x): 
    return x, x**2, x**3

a, b, c = parfor(func1, range(10))
print(a)
print(b)
print(c)


[0 1 2 3 4 5 6 7 8 9]
[ 0  1  4  9 16 25 36 49 64 81]
[  0   1   8  27  64 125 216 343 512 729]

or


In [7]:
result = parallel_map(func1, range(10))
result_array = np.array(result)
print(result_array[:, 0])  # == a
print(result_array[:, 1])  # == b
print(result_array[:, 2])  # == c


[0 1 2 3 4 5 6 7 8 9]
[ 0  1  4  9 16 25 36 49 64 81]
[  0   1   8  27  64 125 216 343 512 729]

Note that the return values are arranged differently for the parallel_map and the parfor functions, as illustrated below. In particular, the return value of parallel_map is not enforced to be NumPy arrays, which can avoid unnecessary copying if all that is needed is to iterate over the resulting list:


In [9]:
result = parfor(func1, range(5))
print(result)

result = parallel_map(func1, range(5))
print(result)


[array([0, 1, 2, 3, 4]), array([ 0,  1,  4,  9, 16]), array([ 0,  1,  8, 27, 64])]
[(0, 0, 0), (1, 1, 1), (2, 4, 8), (3, 9, 27), (4, 16, 64)]

Multi-Variable Functions

The parallel_map and parfor functions are not limited to just numbers, but also works for a variety of outputs:


In [10]:
def func2(x):
    return x, Qobj(x), 'a' * x

a, b, c = parfor(func2, range(5))
print(a)
print(b)
print(c)


[0 1 2 3 4]
[ Quantum object: dims = [[1], [1]], shape = [1, 1], type = oper, isherm = True
Qobj data =
[[ 0.]]
 Quantum object: dims = [[1], [1]], shape = [1, 1], type = oper, isherm = True
Qobj data =
[[ 1.]]
 Quantum object: dims = [[1], [1]], shape = [1, 1], type = oper, isherm = True
Qobj data =
[[ 2.]]
 Quantum object: dims = [[1], [1]], shape = [1, 1], type = oper, isherm = True
Qobj data =
[[ 3.]]
 Quantum object: dims = [[1], [1]], shape = [1, 1], type = oper, isherm = True
Qobj data =
[[ 4.]]]
['' 'a' 'aa' 'aaa' 'aaaa']

In [13]:
result = parallel_map(func2, range(5))
result_array = np.array(result)
print(result_array[:, 0])  # == a
print(result_array[:, 1])  # == b
print(result_array[:, 2])  # == c


[0 1 2 3 4]
[ Quantum object: dims = [[1], [1]], shape = [1, 1], type = oper, isherm = True
Qobj data =
[[ 0.]]
 Quantum object: dims = [[1], [1]], shape = [1, 1], type = oper, isherm = True
Qobj data =
[[ 1.]]
 Quantum object: dims = [[1], [1]], shape = [1, 1], type = oper, isherm = True
Qobj data =
[[ 2.]]
 Quantum object: dims = [[1], [1]], shape = [1, 1], type = oper, isherm = True
Qobj data =
[[ 3.]]
 Quantum object: dims = [[1], [1]], shape = [1, 1], type = oper, isherm = True
Qobj data =
[[ 4.]]]
['' 'a' 'aa' 'aaa' 'aaaa']

One can also define functions with multiple input arguments and even keyword arguments. Here the parallel_map and parfor functions behaves differently: While parallel_map only iterate over the values arguments, the parfor function simultaneously iterates over all arguments:


In [15]:
def sum_diff(x, y, z=0): 
    return x + y, x - y, z

parfor(sum_diff, [1, 2, 3], [4, 5, 6], z=5.0)


Out[15]:
[(array([5, 6, 7]), array([-3, -4, -5]), 5.0),
 (array([6, 7, 8]), array([-2, -3, -4]), 5.0),
 (array([7, 8, 9]), array([-1, -2, -3]), 5.0)]

In [16]:
parallel_map(sum_diff, [1, 2, 3], task_args=(np.array([4, 5, 6]),), task_kwargs=dict(z=5.0))


Out[16]:
[(array([5, 6, 7]), array([-3, -4, -5]), 5.0),
 (array([6, 7, 8]), array([-2, -3, -4]), 5.0),
 (array([7, 8, 9]), array([-1, -2, -3]), 5.0)]

Note that the keyword arguments can be anything you like, but the keyword values are not iterated over. The keyword argument num_cpus is reserved as it sets the number of CPU's used by parfor. By default, this value is set to the total number of physical processors on your system. You can change this number to a lower value, however setting it higher than the number of CPU's will cause a drop in performance. In parallel_map, keyword arguments to the task function are specified using task_kwargs argument, so there is no special reserved keyword arguments.

The parallel_map function also supports progressbar, using the keyword argument progress_bar which can be set to True or to an instance of BaseProgressBar. There is a function called serial_map that works as a non-parallel drop-in replacement for parallel_map, which allows easy switching between serial and parallel computation.


In [9]:
def func(x): 
    return x
result = parallel_map(func, range(50), progress_bar=True)


10.0%. Run time:   0.01s. Est. time left: 00:00:00:00
20.0%. Run time:   0.01s. Est. time left: 00:00:00:00
30.0%. Run time:   0.01s. Est. time left: 00:00:00:00
40.0%. Run time:   0.01s. Est. time left: 00:00:00:00
50.0%. Run time:   0.01s. Est. time left: 00:00:00:00
60.0%. Run time:   0.01s. Est. time left: 00:00:00:00
70.0%. Run time:   0.01s. Est. time left: 00:00:00:00
80.0%. Run time:   0.01s. Est. time left: 00:00:00:00
90.0%. Run time:   0.01s. Est. time left: 00:00:00:00
100.0%. Run time:   0.01s. Est. time left: 00:00:00:00
Total run time:   0.11s

IPython Based Parallel Processing

When QuTiP is used with IPython interpreter, there is an alternative parallel for-loop implementation in the QuTiP module qutip.ipynbtools, see parallel_map. The advantage of this parallel_map implementation is based on IPythons powerful framework for parallelization, so the compute processes are not confined to run on the same host as the main process, i.e. cluster computations.

**Note**: In order to run the IPython `parallel_map` function, you must first turn on the IPython cluster engine.

In [10]:
from qutip.ipynbtools import parallel_map
result = parallel_map(func, range(50), progress_bar=True)


 


In [1]:
from IPython.core.display import HTML
def css_styling():
    styles = open("../styles/guide.css", "r").read()
    return HTML(styles)
css_styling()


Out[1]: