# 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.