In [8]:
import numpy as np
from qutip import *
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:
In [6]:
def func1(x):
return x, x**2, x**3
a, b, c = parfor(func1, range(10))
print(a)
print(b)
print(c)
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
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)
In [10]:
def func2(x):
return x, Qobj(x), 'a' * x
a, b, c = parfor(func2, range(5))
print(a)
print(b)
print(c)
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
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]:
In [16]:
parallel_map(sum_diff, [1, 2, 3], task_args=(np.array([4, 5, 6]),), task_kwargs=dict(z=5.0))
Out[16]:
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)
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.
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]: