# Parallel computation

## Parallel map and parallel for-loop

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 `qutip.solver.parallel.parallel_map` function.

To use the this function 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:

``` >>> result = parallel_map(func1, range(10))

>>> result_array = np.array(result)

>>> print(result_array[:, 0])  # == a
[0 1 2 3 4 5 6 7 8 9]

>>> print(result_array[:, 1])  # == b
[ 0  1  4  9 16 25 36 49 64 81]

>>> print(result_array[:, 2])  # == c
[  0   1   8  27  64 125 216 343 512 729]

>>> print(result)
[(0, 0, 0), (1, 1, 1), (2, 4, 8), (3, 9, 27), (4, 16, 64)]
```

The `qutip.solver.parallel.parallel_map` function is not limited to just numbers, but also works for a variety of outputs:

``` >>> def func2(x): return x, Qobj(x), 'a' * x

>>> results = parallel_map(func2, range(5))

>>> print([result for result in results])
[0 1 2 3 4]

>>> print([result for result in results])
[Quantum object: dims = [, ], shape = (1, 1), type = bra
Qobj data =
[[0.]]
Quantum object: dims = [, ], shape = (1, 1), type = bra
Qobj data =
[[1.]]
Quantum object: dims = [, ], shape = (1, 1), type = bra
Qobj data =
[[2.]]
Quantum object: dims = [, ], shape = (1, 1), type = bra
Qobj data =
[[3.]]
Quantum object: dims = [, ], shape = (1, 1), type = bra
Qobj data =
[[4.]]]

>>>print([result for result in results])
['' 'a' 'aa' 'aaa' 'aaaa']
```

One can also define functions with multiple input arguments and keyword arguments.

```  >>> def sum_diff(x, y, z=0): return x + y, x - y, z

[(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)]
```

The `qutip.solver.parallel.parallel_map` function supports progressbar by setting the keyword argument progress_bar to True. The number of cpu used can also be controlled using the map_kw keyword, per default, all available cpus are used.

``` >>> import time

>>> def func(x): time.sleep(1)

>>> result = parallel_map(func, range(50), progress_bar=True, map_kw={"num_cpus": 2})

10.0%. Run time:   3.10s. Est. time left: 00:00:00:27
20.0%. Run time:   5.11s. Est. time left: 00:00:00:20
30.0%. Run time:   8.11s. Est. time left: 00:00:00:18
40.0%. Run time:  10.15s. Est. time left: 00:00:00:15
50.0%. Run time:  13.15s. Est. time left: 00:00:00:13
60.0%. Run time:  15.15s. Est. time left: 00:00:00:10
70.0%. Run time:  18.15s. Est. time left: 00:00:00:07
80.0%. Run time:  20.15s. Est. time left: 00:00:00:05
90.0%. Run time:  23.15s. Est. time left: 00:00:00:02
100.0%. Run time:  25.15s. Est. time left: 00:00:00:00
Total run time:  28.91s
```

There is a function called `qutip.solver.parallel.serial_map` that works as a non-parallel drop-in replacement for `qutip.solver.parallel.parallel_map`, which allows easy switching between serial and parallel computation. Qutip also has the function `qutip.solver.parallel.loky_map` as another drop-in replacement. It use the loky module instead of multiprocessing to run in parallel. Parallel processing is useful for repeated tasks such as generating plots corresponding to the dynamical evolution of your system, or simultaneously simulating different parameter configurations.

## IPython-based parallel_map

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