Saving QuTiP Objects and Data Sets

With time-consuming calculations it is often necessary to store the results to files on disk, so it can be post-processed and archived. In QuTiP there are two facilities for storing data: Quantum objects can be stored to files and later read back as python pickles, and numerical data (vectors and matrices) can be exported as plain text files in for example CSV (comma-separated values), TSV (tab-separated values), etc. The former method is preferred when further calculations will be performed with the data, and the latter when the calculations are completed and data is to be imported into a post-processing tool (e.g. for generating figures).

Storing and loading QuTiP objects

To store and load arbitrary QuTiP related objects (qutip.Qobj, qutip.solver.Result, etc.) there are two functions: qutip.fileio.qsave and qutip.fileio.qload. The function qutip.fileio.qsave takes an arbitrary object as first parameter and an optional filename as second parameter (default filename is qutip_data.qu). The filename extension is always .qu. The function qutip.fileio.qload takes a mandatory filename as first argument and loads and returns the objects in the file.

To illustrate how these functions can be used, consider a simple calculation of the steadystate of the harmonic oscillator:

In [1]: a = destroy(10); H = a.dag() * a ; c_ops = [np.sqrt(0.5) * a, np.sqrt(0.25) * a.dag()]

In [2]: rho_ss = steadystate(H, c_ops)

The steadystate density matrix rho_ss is an instance of qutip.Qobj. It can be stored to a file steadystate.qu using

In [3]: qsave(rho_ss, 'steadystate')

In [4]: ls *.qu
density_matrix_vs_time.qu  steadystate.qu

and it can later be loaded again, and used in further calculations:

In [5]: rho_ss_loaded = qload('steadystate')
Loaded Qobj object:
Quantum object: dims = [[10], [10]], shape = (10, 10), type = oper, isHerm = True


In [6]: a = destroy(10)

In [7]: expect(a.dag() * a, rho_ss_loaded)
Out[7]: 0.9902248289345063

The nice thing about the qutip.fileio.qsave and qutip.fileio.qload functions is that almost any object can be stored and load again later on. We can for example store a list of density matrices as returned by qutip.mesolve:

In [8]: a = destroy(10); H = a.dag() * a ; c_ops = [np.sqrt(0.5) * a, np.sqrt(0.25) * a.dag()]

In [9]: psi0 = rand_ket(10)

In [10]: times = np.linspace(0, 10, 10)

In [11]: dm_list = mesolve(H, psi0, times, c_ops, [])

In [12]: qsave(dm_list, 'density_matrix_vs_time')

And it can then be loaded and used again, for example in an other program:

In [13]: dm_list_loaded = qload('density_matrix_vs_time')
Loaded Result object:
Result object with mesolve data.
--------------------------------
states = True
num_collapse = 0

In [14]: a = destroy(10)

In [15]: expect(a.dag() * a, dm_list_loaded.states)
Out[15]: 
array([4.04416292, 3.22336973, 2.64206514, 2.2194952 , 1.90787059,
       1.67647333, 1.50399675, 1.37514572, 1.27874735, 1.20656019])

Storing and loading datasets

The qutip.fileio.qsave and qutip.fileio.qload are great, but the file format used is only understood by QuTiP (python) programs. When data must be exported to other programs the preferred method is to store the data in the commonly used plain-text file formats. With the QuTiP functions qutip.fileio.file_data_store and qutip.fileio.file_data_read we can store and load numpy arrays and matrices to files on disk using a deliminator-separated value format (for example comma-separated values CSV). Almost any program can handle this file format.

The qutip.fileio.file_data_store takes two mandatory and three optional arguments:

>>> file_data_store(filename, data, numtype="complex", numformat="decimal", sep=",")

where filename is the name of the file, data is the data to be written to the file (must be a numpy array), numtype (optional) is a flag indicating numerical type that can take values complex or real, numformat (optional) specifies the numerical format that can take the values exp for the format 1.0e1 and decimal for the format 10.0, and sep (optional) is an arbitrary single-character field separator (usually a tab, space, comma, semicolon, etc.).

A common use for the qutip.fileio.file_data_store function is to store the expectation values of a set of operators for a sequence of times, e.g., as returned by the qutip.mesolve function, which is what the following example does:

In [16]: a = destroy(10); H = a.dag() * a ; c_ops = [np.sqrt(0.5) * a, np.sqrt(0.25) * a.dag()]

In [17]: psi0 = rand_ket(10)

In [18]: times = np.linspace(0, 100, 100)

In [19]: medata = mesolve(H, psi0, times, c_ops, [a.dag() * a, a + a.dag(), -1j * (a - a.dag())])

In [20]:    shape(medata.expect)
Out[20]: (3, 100)

In [21]: shape(times)
Out[21]: (100,)

In [22]: output_data = np.vstack((times, medata.expect))   # join time and expt data

In [23]: file_data_store('expect.dat', output_data.T) # Note the .T for transpose!

In [24]: ls *.dat
expect.dat

In [25]: !head expect.dat
# Generated by QuTiP: 100x4 complex matrix in decimal format [',' separated values].
0.0000000000+0.0000000000j,3.8999160647+0.0000000000j,-0.3698450405+0.0000000000j,-0.0386004040+0.0000000000j
1.0101010101+0.0000000000j,3.1665288000+0.0000000000j,-0.1086153265+0.0000000000j,0.2308584366+0.0000000000j
2.0202020202+0.0000000000j,2.6416361771+0.0000000000j,0.1101685451+0.0000000000j,0.1748708584+0.0000000000j
3.0303030303+0.0000000000j,2.2509491702+0.0000000000j,0.1735585411+0.0000000000j,0.0018155507+0.0000000000j
4.0404040404+0.0000000000j,1.9557058217+0.0000000000j,0.0813409326+0.0000000000j,-0.1241158971+0.0000000000j
5.0505050505+0.0000000000j,1.7309805073+0.0000000000j,-0.0526534378+0.0000000000j,-0.1167630295+0.0000000000j
6.0606060606+0.0000000000j,1.5592391355+0.0000000000j,-0.1100362781+0.0000000000j,-0.0156754518+0.0000000000j
7.0707070707+0.0000000000j,1.4276628988+0.0000000000j,-0.0627253570+0.0000000000j,0.0736668399+0.0000000000j
8.0808080808+0.0000000000j,1.3266948516+0.0000000000j,0.0251410605+0.0000000000j,0.0805582320+0.0000000000j

In this case we didn’t really need to store both the real and imaginary parts, so instead we could use the numtype=”real” option:

In [26]: file_data_store('expect.dat', output_data.T, numtype="real")

In [27]: !head -n5 expect.dat
# Generated by QuTiP: 100x4 real matrix in decimal format [',' separated values].
0.0000000000,3.8999160647,-0.3698450405,-0.0386004040
1.0101010101,3.1665288000,-0.1086153265,0.2308584366
2.0202020202,2.6416361771,0.1101685451,0.1748708584
3.0303030303,2.2509491702,0.1735585411,0.0018155507

and if we prefer scientific notation we can request that using the numformat=”exp” option

In [28]: file_data_store('expect.dat', output_data.T, numtype="real", numformat="exp")

In [29]: !head -n 5 expect.dat
# Generated by QuTiP: 100x4 real matrix in exp format [',' separated values].
0.0000000000e+00,3.8999160647e+00,-3.6984504055e-01,-3.8600403996e-02
1.0101010101e+00,3.1665288000e+00,-1.0861532645e-01,2.3085843662e-01
2.0202020202e+00,2.6416361771e+00,1.1016854507e-01,1.7487085839e-01
3.0303030303e+00,2.2509491702e+00,1.7355854113e-01,1.8155506866e-03

Loading data previously stored using qutip.fileio.file_data_store (or some other software) is a even easier. Regardless of which deliminator was used, if data was stored as complex or real numbers, if it is in decimal or exponential form, the data can be loaded using the qutip.fileio.file_data_read, which only takes the filename as mandatory argument.

In [30]: input_data = file_data_read('expect.dat')

In [31]: shape(input_data)
Out[31]: (100, 4)

In [32]: plot(input_data[:,0], input_data[:,1]);  # plot the data
../images/saving_ex.png

(If a particularly obscure choice of deliminator was used it might be necessary to use the optional second argument, for example sep=”_” if _ is the deliminator).