Learn quantum spin, entanglement, and quantum computer operations

## Project description

This is a little package that will help with learning how quantum spin and entanglement work. It is meant to complement some of the “theoretical minimum” lectures and other web resources:

• Book: Quantum Mechanics - The Theoretical Minimum, Leanoard Susskind and Art Friedman, Basic Books, 2014. (mostly chapters 6&7)

## Install

pip install --upgrade qspin

## Out-of-the box tests:

\$ python
>>> import qspin
>>> qspin.all_tests()

## Examples of code use

### Spin states

up, down and linear combinations to form mixed states

>>> from qspin import bra,ket,u,d,s0,sx,sy,sz
>>> u
|u>
>>> d
|d>
>>> u + d
|u>  + |d>
>>> i = 1j
>>> u + i*d
|u>  + i |d>

### Operators

>>> sx # Pauli spin matrix
[[0 1]
[1 0]]
>>> sy
[[ 0.+0.j -0.-1.j]
[ 0.+1.j  0.+0.j]]
>>> sz
[[ 1  0]
[ 0 -1]]
>>> sz*u
|u>
>>> sz*d
- |d>

### Expected value of an observable

sz is the observable for z-component of spin, For the “up” state, the only outcome us +1, so the expected value is +1.

>> u.H*sz*u
1.0
>> sz.average(u) # another way to compute the average of an observable
1.0

(q.H is Hermetian conjugate; it converts a ket to a bra, as in $$\Braket{u|s_z|u}$$). The operator (sz in this case) is known in quantum mechanics as an observable, meaning it measures something. Here it is the z-component of spin. The eigenvalues of the observable are the possible outcomes the observation. Underlying each state is a wave function. We store the wave function internally as vector, with each component being the wave function value for the basis eigenstate. The operators (observables) are stored as matrices, also defined on the same basis. The assumed basis throughout qspin is $$\Ket{u}$$ and $$\Ket{d}$$ for single particles.

>>> u
|u>
>>> u.phi
matrix([[ 1.],
[ 0.]])

### Eigenvalues

We can evaluate the eigenvalues and eigenvectors of observables. “.matrix” pulls out the matrix representation of the operator.

>>> import numpy as np
>>> sz
[[ 1  0]
[ 0 -1]]
>>> ev, evec = np.linalg.eig(sz.matrix)
>>> ev
array([ 1., -1.])
>>> evec
matrix([[ 1.,  0.],
[ 0.,  1.]])
>>> sx # spin x
[[0 1]
[1 0]]
>>> ev, evec = np.linalg.eig(sx.matrix)
>>> ev
array([ 1., -1.])
>>> evec
matrix([[ 0.70710678, -0.70710678],
[ 0.70710678,  0.70710678]])

There is a handy ‘eig’ method that produces a list of eigenvalues and a list of eigenvectors, with the eigenvectors being states:

>>> ev, evec = sx.eig()
>>> ev
array([1.,=1.])
>>> evec
[0.707107 |u> + 0.707107 |d> , -0.707107 |u> + 0.707107 |d> ]
>>> sz.eig()
(array([ 1., -1.]), [|u> , |d> ])

Note that the spin-x observerable has the same eigenvalues as spin-z, +1 and -1. But the eigenvectors are different, in our basis, since we are using the {$$\Ket{u}$$, $$\Ket{d}$$} basis. They are $$(\Ket{u} + \Ket{d})/\sqrt{2}$$, which measures as sx = +1, and $$(\Ket{u} - \Ket{d})/\sqrt{2}$$, which measures as sx = -1.

### Conditional Probabilities

Conditional probabilities are calculated using inner products of states with the eigenvectors of the measurment, squared. So the probability of measuring sx = +1 given the particle is prepared in state $$\Ket{u}$$ is:

>>> l = (u+d).N # "left" state. The .N normalizes the state
>>> (bra(l)*ket(u))**2   # expected value of up given left
0.5
>>> np.abs( l.H * u )**2 # another way to do this. The .H means Hermetian conjugate; converts ket to bra
0.5
>>> l.prob(sx,l)
1.0
>>> l.prob(sx,u)
0.5

### Measurement

The quantum measurement of an observable involves ‘collapsing’ the state to one of the eigen states of the obserable.

>>> l = (u+d).N
>>> sz.measure(l)
(1.0, |u>)

The result is random, either up or down (with 50-50 probability in this case where the particle starts out in state ‘spin left’). The measure function returns the value of the measurment, 1.0 in this case, and the collapsed state, $$\Ket{u}$$.

### String Representation of State

We can use strings to refer to basis states.

>>> u = ket('|u>') # or ket('u')  (the vert line and bracket are optional)
>>> d = ket('|d>') # or ket('d')
>>> u
|u>
>>> d
|d>

The string representation of basis functions defaults to ‘u’ and ‘d’. As an alternative, the representation can be set to ‘0’ and ‘1’ or to up and down arrows (the later require your terminal to have the ability to display unicode characters).

>>> qspin.set_base_repr('01')
>>> u = ket('0')
>>> d = ket('1')
>>> (u + d).N
0.707107 |0> + 0.707107 |1>

With qspin.set_base_repr('arrow'), u=ket([1,0]) renders as $$\Ket{\uparrow}$$ This provides cute printout, but is not too useful for string entry, since the up and down arrows are unicode.

### Wave Function Definition

States can also be defined using the wave function, given in the form of a matrix column vector. And it is good practice to normalize states.

>>> w = ket( np.matrix([1.,1.]).T).N
>>> w
0.707106781187 |u>  + 0.707106781187 |d>

Form a projection operator from outer products of basis states.

>> rho = ket('|u>') * bra('<u|') + ket('|d>') * bra('<d|')
>> # can also do this:
>> u = ket('|u>'); d = ket('|d>');
>> rho = ket(u) * bra(u) + ket(d) * bra(d)
>>> rho
[[ 1.  0.]
[ 0.  1.]]
>>> u
1.0 |u>
>>> rho*u
1.0 |u>
>>> rho*d
1.0 |d>

Note that bra(ket(…)) and ket(bra(…)) convert, and takes care of the complex-conjugating.

>> u.kind
'ket'
>> bra(u).kind
'bra'

### Density Matrix and Entropy

Create a density matrix for an ensemble of single particles.

>> from qspin import entropy
>> P = [0.5, 0.5]
>> rho = P[0] * bra('|u>').density() + P[1] * bra('|d>').density() # make sure the probabilities add to one
>> entropy(rho) # it's not zero because there is a mixture of states
0.69314718055994529
>> rho = ( bra('|u>') + bra('|d>') ).N.density()
>> entropy(rho) # this is zero because all electrons are prepared in the "u+d" state
0

Make sure you normalize any states you define, using the post-operation .N.

The von Neumann entropy is $$S = -\sum_i(p_i log(p_i))$$ where $$p_i$$ are the density matrix eigenvalues. The entropy is essentially the randomness in a measurement of the quantum state. It can be applied to any density matrix for either pure or mixed states. (A pure state has zero entropy.)

### Multi-particle States

Multi-particle states are in the space formed from the Kronecker product of Hilbert spaces of the individual particles. Since multi-particle quantum states can be mixed states, there are far more possible state vectors ($$2^n$$ dimensional vector space) than for classical systems (which are in only $$n$$ dimensional space)

We build up multi-particle states with Kronecker products ‘**’ (meaning $$\otimes$$), or with strings

>>> uu = u**u
>>> dd = ket('|dd>') # or ket('dd')
>>> s = (d**u**u + u**d**u + d**d**u).N
>>> s
0.57735 |udu> + 0.57735 |duu> + 0.57735 |ddu>

Multi-particle operators are similarly built up with Kronecker products

>>> s2x = sx**sx
>>> s2x
[[0 0 0 1]
[0 0 1 0]
[0 1 0 0]
[1 0 0 0]

### Partial Trace

The density matrix for a multi-particle state is $$2^n \times 2^n$$. A partial trace is a way to form the density matrix for a subset of the particles. ‘Tracing out’ $$m$$ of the particles results in a $$2^{n-m} \times 2^{n-m}$$ density matrix. Partial traces are important in many aspects of analyzing the multi-particle state, including evaluating the entanglement.

>>> sing = (u**d - d**u).N
>>> rho = sing.density()
>>> rho
matrix([[ 0. ,  0. ,  0. ,  0. ],
[ 0. ,  0.5, -0.5,  0. ],
[ 0. , -0.5,  0.5,  0. ],
[ 0. ,  0. ,  0. ,  0. ]])
>>> rhoA = ptrace(rho,[1]) # trace over particle 1 ('Bob') to get particle 0 ('Alice') density
>>> rhoA
matrix([[0.5, 0. ],
[0. , 0.5]])

### Entangled States

Once you have created a (possibly) entangled state of two particles, you can test it for entanglement:

>>> sing = (u**d - d**u).N
>>> sing.entangled()
True
>>> (u**u).entangled()
False

The test for entanglement is to check the entropy of one of the particles after the other particle has been ‘traced out.’

### Quantum Computing

Several quantum logic gates are now defined in qspin including: Hadamard, NOT, SWAP, controlled gates, square root gates, and phase shift gates.

>>> from qspin import u,d,gate
>>> SWAP = gate('SWAP')
>>> SWAP*(u**d)
|du>
>>> H*u
0.707 |u> + 0.707 |d>
>>> H*d
0.707 |u> - 0.707 |d>

shows that SWAP interchanges the q-bits, and Hadamard makes the Bell states from spin up and spin down.

## Project details

Uploaded source
Uploaded py3