Manipulating States and Operators¶
Introduction¶
In the previous guide section Basic Operations on Quantum Objects, we saw how to create states and operators, using the functions built into QuTiP. In this portion of the guide, we will look at performing basic operations with states and operators. For more detailed demonstrations on how to use and manipulate these objects, see the examples on the tutorials web page.
State Vectors (kets or bras)¶
Here we begin by creating a Fock qutip.states.basis
vacuum state vector \(\left|0\right>\) with in a Hilbert space with 5 number states, from 0 to 4:
In [1]: vac = basis(5, 0)
In [2]: vac
Out[2]:
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 1.]
[ 0.]
[ 0.]
[ 0.]
[ 0.]]
and then create a lowering operator \(\left(\hat{a}\right)\) corresponding to 5 number states using the qutip.operators.destroy
function:
In [3]: a = destroy(5)
In [4]: a
Out[4]:
Quantum object: dims = [[5], [5]], shape = (5, 5), type = oper, isherm = False
Qobj data =
[[ 0. 1. 0. 0. 0. ]
[ 0. 0. 1.41421356 0. 0. ]
[ 0. 0. 0. 1.73205081 0. ]
[ 0. 0. 0. 0. 2. ]
[ 0. 0. 0. 0. 0. ]]
Now lets apply the destruction operator to our vacuum state vac
,
In [5]: a * vac
Out[5]:
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0.]
[ 0.]
[ 0.]
[ 0.]
[ 0.]]
We see that, as expected, the vacuum is transformed to the zero vector. A more interesting example comes from using the adjoint of the lowering operator, the raising operator \(\hat{a}^\dagger\):
In [6]: a.dag() * vac
Out[6]:
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0.]
[ 1.]
[ 0.]
[ 0.]
[ 0.]]
The raising operator has in indeed raised the state vec from the vacuum to the \(\left| 1\right>\) state. Instead of using the dagger Qobj.dag()
method to raise the state, we could have also used the built in qutip.operators.create
function to make a raising operator:
In [7]: c = create(5)
In [8]: c * vac
Out[8]:
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0.]
[ 1.]
[ 0.]
[ 0.]
[ 0.]]
which does the same thing. We can raise the vacuum state more than once by successively apply the raising operator:
In [9]: c * c * vac
Out[9]:
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0. ]
[ 0. ]
[ 1.41421356]
[ 0. ]
[ 0. ]]
or just taking the square of the raising operator \(\left(\hat{a}^\dagger\right)^{2}\):
In [10]: c ** 2 * vac
Out[10]:
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0. ]
[ 0. ]
[ 1.41421356]
[ 0. ]
[ 0. ]]
Applying the raising operator twice gives the expected \(\sqrt{n + 1}\) dependence. We can use the product of \(c * a\) to also apply the number operator to the state vector vac
:
In [11]: c * a * vac
Out[11]:
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0.]
[ 0.]
[ 0.]
[ 0.]
[ 0.]]
or on the \(\left| 1\right>\) state:
In [12]: c * a * (c * vac)
Out[12]:
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0.]
[ 1.]
[ 0.]
[ 0.]
[ 0.]]
or the \(\left| 2\right>\) state:
In [13]: c * a * (c**2 * vac)
Out[13]:
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0. ]
[ 0. ]
[ 2.82842712]
[ 0. ]
[ 0. ]]
Notice how in this last example, application of the number operator does not give the expected value \(n=2\), but rather \(2\sqrt{2}\). This is because this last state is not normalized to unity as \(c\left| n\right> = \sqrt{n+1}\left| n+1\right>\). Therefore, we should normalize our vector first:
In [14]: c * a * (c**2 * vac).unit()
Out[14]:
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0.]
[ 0.]
[ 2.]
[ 0.]
[ 0.]]
Since we are giving a demonstration of using states and operators, we have done a lot more work than we should have. For example, we do not need to operate on the vacuum state to generate a higher number Fock state. Instead we can use the qutip.states.basis
(or qutip.states.fock
) function to directly obtain the required state:
In [15]: ket = basis(5, 2)
In [16]: print(ket)
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0.]
[ 0.]
[ 1.]
[ 0.]
[ 0.]]
Notice how it is automatically normalized. We can also use the built in qutip.operators.num
operator:
In [17]: n = num(5)
In [18]: print(n)
Quantum object: dims = [[5], [5]], shape = (5, 5), type = oper, isherm = True
Qobj data =
[[ 0. 0. 0. 0. 0.]
[ 0. 1. 0. 0. 0.]
[ 0. 0. 2. 0. 0.]
[ 0. 0. 0. 3. 0.]
[ 0. 0. 0. 0. 4.]]
Therefore, instead of c * a * (c ** 2 * vac).unit()
we have:
In [19]: n * ket
Out[19]:
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0.]
[ 0.]
[ 2.]
[ 0.]
[ 0.]]
We can also create superpositions of states:
In [20]: ket = (basis(5, 0) + basis(5, 1)).unit()
In [21]: print(ket)
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0.70710678]
[ 0.70710678]
[ 0. ]
[ 0. ]
[ 0. ]]
where we have used the qutip.Qobj.unit
method to again normalize the state. Operating with the number function again:
In [22]: n * ket
Out[22]:
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0. ]
[ 0.70710678]
[ 0. ]
[ 0. ]
[ 0. ]]
We can also create coherent states and squeezed states by applying the qutip.operators.displace
and qutip.operators.squeeze
functions to the vacuum state:
In [23]: vac = basis(5, 0)
In [24]: d = displace(5, 1j)
In [25]: s = squeeze(5, 0.25 + 0.25j)
In [26]: d * vac
Out[26]:
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0.60655682+0.j ]
[ 0.00000000+0.60628133j]
[-0.43038740+0.j ]
[ 0.00000000-0.24104351j]
[ 0.14552147+0.j ]]
In [27]: d * s * vac
Out[27]:
Quantum object: dims = [[5], [1]], shape = (5, 1), type = ket
Qobj data =
[[ 0.65893786+0.08139381j]
[ 0.10779462+0.51579735j]
[-0.37567217-0.01326853j]
[-0.02688063-0.23828775j]
[ 0.26352814+0.11512178j]]
Of course, displacing the vacuum gives a coherent state, which can also be generated using the built in qutip.states.coherent
function.
Density matrices¶
One of the main purpose of QuTiP is to explore the dynamics of open quantum systems, where the most general state of a system is not longer a state vector, but rather a density matrix. Since operations on density matrices operate identically to those of vectors, we will just briefly highlight creating and using these structures.
The simplest density matrix is created by forming the outer-product \(\left|\psi\right>\left<\psi\right|\) of a ket vector:
In [28]: ket = basis(5, 2)
In [29]: ket * ket.dag()
Out[29]:
Quantum object: dims = [[5], [5]], shape = (5, 5), type = oper, isherm = True
Qobj data =
[[ 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0.]
[ 0. 0. 1. 0. 0.]
[ 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0.]]
A similar task can also be accomplished via the qutip.states.fock_dm
or qutip.states.ket2dm
functions:
In [30]: fock_dm(5, 2)
Out[30]:
Quantum object: dims = [[5], [5]], shape = (5, 5), type = oper, isherm = True
Qobj data =
[[ 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0.]
[ 0. 0. 1. 0. 0.]
[ 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0.]]
In [31]: ket2dm(ket)
Out[31]:
Quantum object: dims = [[5], [5]], shape = (5, 5), type = oper, isherm = True
Qobj data =
[[ 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0.]
[ 0. 0. 1. 0. 0.]
[ 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0.]]
If we want to create a density matrix with equal classical probability of being found in the \(\left|2\right>\) or \(\left|4\right>\) number states we can do the following:
In [32]: 0.5 * ket2dm(basis(5, 4)) + 0.5 * ket2dm(basis(5, 2))
Out[32]:
Quantum object: dims = [[5], [5]], shape = (5, 5), type = oper, isherm = True
Qobj data =
[[ 0. 0. 0. 0. 0. ]
[ 0. 0. 0. 0. 0. ]
[ 0. 0. 0.5 0. 0. ]
[ 0. 0. 0. 0. 0. ]
[ 0. 0. 0. 0. 0.5]]
or use 0.5 * fock_dm(5, 2) + 0.5 * fock_dm(5, 4)
. There are also several other built-in functions for creating predefined density matrices, for example qutip.states.coherent_dm
and qutip.states.thermal_dm
which create coherent state and thermal state density matrices, respectively.
In [33]: coherent_dm(5, 1.25)
Out[33]:
Quantum object: dims = [[5], [5]], shape = (5, 5), type = oper, isherm = True
Qobj data =
[[ 0.20980701 0.26141096 0.23509686 0.15572585 0.13390765]
[ 0.26141096 0.32570738 0.29292109 0.19402805 0.16684347]
[ 0.23509686 0.29292109 0.26343512 0.17449684 0.1500487 ]
[ 0.15572585 0.19402805 0.17449684 0.11558499 0.09939079]
[ 0.13390765 0.16684347 0.1500487 0.09939079 0.0854655 ]]
In [34]: thermal_dm(5, 1.25)
Out[34]:
Quantum object: dims = [[5], [5]], shape = (5, 5), type = oper, isherm = True
Qobj data =
[[ 0.46927974 0. 0. 0. 0. ]
[ 0. 0.26071096 0. 0. 0. ]
[ 0. 0. 0.14483942 0. 0. ]
[ 0. 0. 0. 0.08046635 0. ]
[ 0. 0. 0. 0. 0.04470353]]
QuTiP also provides a set of distance metrics for determining how close two density matrix distributions are to each other. Included are the trace distance qutip.metrics.tracedist
, fidelity qutip.metrics.fidelity
, Hilbert-Schmidt distance qutip.metrics.hilbert_dist
, Bures distance qutip.metrics.bures_dist
, and Bures angle qutip.metrics.bures_angle
.
In [35]: x = coherent_dm(5, 1.25)
In [36]: y = coherent_dm(5, 1.25j) # <-- note the 'j'
In [37]: z = thermal_dm(5, 0.125)
In [38]: fidelity(x, x)
Out[38]: 1.0000000153373552
In [39]: tracedist(y, y)