Basics
Contents
2. Basics#
2.1. Basic Representation#
States and operators in quimb
are simply dense numpy arrays
or sparse scipy matrices. All functions should directly work with these
but the class qarray
is also provided as a very
thin subclass of numpy.ndarray
with a few helpful methods and
attributes. The quimbify()
function (aliased to
qu()
) can convert between the various representations.
[1]:
from quimb import *
data = [1, 2j, -3]
Kets are column vectors, i.e. with shape (d, 1)
:
[2]:
qu(data, qtype='ket')
[2]:
qarray([[ 1.+0.j],
[ 0.+2.j],
[-3.+0.j]])
The normalized=True
option can be used to ensure a normalized output.
Bras are row vectors, i.e. with shape (1, d)
:
[3]:
qu(data, qtype='bra') # also conjugates the data
[3]:
qarray([[ 1.-0.j, 0.-2.j, -3.-0.j]])
And operators are square matrices, i.e. have shape (d, d)
:
[4]:
qu(data, qtype='dop')
[4]:
qarray([[ 1.+0.j, 0.-2.j, -3.-0.j],
[ 0.+2.j, 4.+0.j, 0.-6.j],
[-3.+0.j, 0.+6.j, 9.+0.j]])
Which can also be sparse:
[5]:
qu(data, qtype='dop', sparse=True)
[5]:
<3x3 sparse matrix of type '<class 'numpy.complex128'>'
with 9 stored elements in Compressed Sparse Row format>
The sparse format can be specified with the stype
keyword. The partial
function versions of each of the above are also available:
Note
If a simple 1d-list is supplied and no qtype
is given, 'ket'
is
assumed.
2.2. Basic Operations#
The ‘dagger’, or hermitian conjugate, operation is performed with the .H
attribute:
[6]:
psi = 1.0j * bell_state('psi-')
psi
[6]:
qarray([[ 0.+0.j ],
[ 0.+0.707107j],
[-0.-0.707107j],
[ 0.+0.j ]])
[7]:
psi.H
[7]:
qarray([[ 0.-0.j , 0.-0.707107j, -0.+0.707107j, 0.-0.j ]])
This is just the combination of .conj()
and .T
, but only available for
scipy.sparse
matrices and qarray
s (not
numpy.ndarray
s).
The product of two quantum objects is the dot or matrix product, which, since
python 3.5, has been overloaded with the @
symbol. Using it is recommended:
[8]:
psi = up()
psi
[8]:
qarray([[1.+0.j],
[0.+0.j]])
[9]:
psi.H @ psi # inner product
[9]:
qarray([[1.+0.j]])
[10]:
X = pauli('X')
X @ psi # act as gate
[10]:
qarray([[0.+0.j],
[1.+0.j]])
[11]:
psi.H @ X @ psi # operator expectation
[11]:
qarray([[0.+0.j]])
Scalar expectation values might best be computed using the
expectation()
function (aliased to
expec()
) which dispatches to accelerated
methods:
[12]:
expec(psi, psi)
[12]:
1.0
[13]:
expec(psi, X)
[13]:
0j
Here’s an example for a much larger (20 qubit), sparse operator expecation, which will be automatically parallelized:
[14]:
psi = rand_ket(2**20)
A = rand_herm(2**20, sparse=True) + speye(2**20)
A
[14]:
<1048576x1048576 sparse matrix of type '<class 'numpy.complex128'>'
with 11534284 stored elements in Compressed Sparse Row format>
[15]:
expec(A, psi) # should be ~ 1
[15]:
0.9999672709199712
[16]:
%%timeit
expec(A, psi)
117 ms ± 10.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
2.3. Combining Objects - Tensoring#
There are a number of ways to combine states and operators, i.e. tensoring them together.
Functional form using kron()
:
>>> kron(psi1, psi2, psi3, ...)
...
This can also be done using the &
overload on qarray
and scipy matrices:
>>> psi1 & psi2 & psi3
...
Warning
When quimb
is imported, it monkey patches the otherwise unused
method of &
/__and__
of scipy sparse matrices to kron()
.
Often one wants to sandwich an operator with many identities,
ikron()
can be used for this:
[17]:
dims = [2] * 10 # overall space of 10 qubits
X = pauli('X')
IIIXXIIIII = ikron(X, dims, inds=[3, 4]) # act on 4th and 5th spin only
IIIXXIIIII.shape
[17]:
(1024, 1024)
For more advanced tensor constructions, such as reversing and interleaving
identities within operators pkron()
can be used:
[18]:
dims = [2] * 3
XZ = pauli('X') & pauli('Z')
ZIX = pkron(XZ, dims, inds=[2, 0])
ZIX.real.astype(int)
[18]:
qarray([[ 0, 1, 0, 0, 0, 0, 0, 0],
[ 1, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 1, 0, 0, 0, 0],
[ 0, 0, 1, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, -1, 0, 0],
[ 0, 0, 0, 0, -1, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, -1],
[ 0, 0, 0, 0, 0, 0, -1, 0]])
ZIX
would then act with Z on first spin, and X on 3rd.
2.4. Removing Objects - Partial Trace#
To remove, or ignore, certain parts of a quantum state the partial trace
function partial_trace()
(aliased to ptr()
)
is used. Here, the internal dimensions of a state must be supplied as well as
the indicies of which of these subsystems to keep.
For example, if we have a random system of 10 qubits (hilbert space of dimension
2**10
), and we want just the reduced density matrix describing the first and
last spins:
[19]:
dims = [2] * 10
D = prod(dims)
psi = rand_ket(D)
rho_ab = ptr(psi, dims, [0, 9])
rho_ab.round(3) # probably pretty close to identity
[19]:
qarray([[ 0.252+0.j , -0.002+0.005j, 0.006+0.014j, -0.014+0.004j],
[-0.002-0.005j, 0.246+0.j , 0.001+0.003j, 0.013+0.029j],
[ 0.006-0.014j, 0.001-0.003j, 0.247+0.j , 0.008+0.01j ],
[-0.014-0.004j, 0.013-0.029j, 0.008-0.01j , 0.254+0.j ]])
partial_trace()
accepts dense or sparse, operators or vectors.