# Quickstart¶

Welcome to the quickstart guide of QuAlg. This guide will go over some of the basic functionalities of the QuAlg-package to get you started. If you wish to use this guide interactively you can find the jupyter notebook at https://github.com/AckslD/QuAlg/blob/master/docs/quickstart.ipynb.

The full API for the package can be found at https://acksld.github.io/QuAlg/api.html.

In this quickstart guide we will go over:

• How to create and work with quantum states of different types.

• How to create and work with operators.

• How to measure states.

For a more elaborate and realistic usecase for QuAlg, see the example https://github.com/AckslD/QuAlg/blob/master/examples/example_ll_povm.py which computes the POVMs used in the simulations of the paper https://arxiv.org/abs/1903.09778 which was preciously computed by hand.

# Variables¶

The power of QuAlg is that one can use variables instead of numbers to represent quantum states, operators etc. Lets first look at how one can work with variables in QuAlg.

A variable can be created as follows:

[1]:

from qualg.scalars import Variable

# A variable 'a'
a = Variable('a')
print(a)

a


A variable is assumed to be a complex number so we can conjugate it as follows

[2]:

print(a.conjugate())

(a*)


QuAlg will also recognise certain expression, such as when taking the product of something conjugate:

[3]:

expr = a * a.conjugate()
print(expr)

|a|^2


One can easily create aritmetic expression using the standard addition and multiplication operators:

[4]:

# Create four variables
a, b, c, d = (Variable(name) for name in ['a', 'b', 'c', 'd'])
expr = (a + b) * (c + d)
print(expr)

(a + b)*(c + d)


Expressions can be simplified using simplify or in expanded to only have sums of product:

[5]:

from qualg.toolbox import simplify, expand
expr = expand(expr)
print("Expanded expression:")
print(expr)

expr = simplify(expr)
print("Simplified expression:")
print(expr)

Expanded expression:
(0 + 0*c + 0*d + 0*a + a*c + a*d + 0*b + b*c + b*d)
Simplified expression:
(a*c + a*d + b*c + b*d)


## States¶

### Single-qubit state¶

We first look at how one can create qubit states:

[6]:

import numpy as np
from qualg.q_state import BaseQubitState


A BaseQuditState (subclass of BaseState) represents a single term in a quantum state and can be used to build a basis for such.

[7]:

# Create a basis for single-qubit states
bs = [BaseQubitState(i) for i in '01']
print(bs[0])
print(bs[1])

|0>
|1>


Using these base states we can construct arbitary states. To do so we first need to make these base states actual states, which we can then add together to construct superpositions of such.

[8]:

s0 = bs[0].to_state()
s1 = bs[1].to_state()


Lets now for example construct the state $$|+\rangle=\frac{1}{2}(|0\rangle+|1\rangle)$$:

[9]:

h0 = (s0 + s1) * (1 / np.sqrt(2))
print(h0)

0.7071067811865475*|0> + 0.7071067811865475*|1>


As you will see when working with QuAlg is that all objects have a nice string-representation and can be printed. This is one of the aims of QuAlg.

We can also make use of variables as we have seen above to represent states symbolically:

[35]:

a = Variable('a')
b = Variable('b')
s_psi = (a * s0 + b * s1)
print(s_psi)

a*|0> + b*|1>


We can then compute the inner norm of this state, that is the inner-product with itself:

[36]:

inner = s_psi.inner_product(s_psi)
print(inner)

(|a|^2 + |b|^2)


### Multi-qudit states¶

Lets say that we instead would like to work with states on two qutrits. We can easily do this as follows:

[11]:

from itertools import product
from qualg.states import State
from qualg.q_state import BaseQuditState

[12]:

levels = 3
bs = [BaseQuditState(f"{i}{j}", base=levels) for i, j in product(range(levels), range(levels))]
print(bs[0])
print(bs[-1])

|00>
|22>


Feel free to change the number of levels above. Similarly to before we can now construct superpositions of these base states:

[13]:

superpos = sum([b.to_state() for b in bs], State()) * (1 / levels)
print(superpos)

0.3333333333333333*|00> + 0.3333333333333333*|01> + 0.3333333333333333*|02> + 0.3333333333333333*|10> + 0.3333333333333333*|11> + 0.3333333333333333*|12> + 0.3333333333333333*|20> + 0.3333333333333333*|21> + 0.3333333333333333*|22>


### Fock states¶

We can also work with states in second quantization, where these are represented as excitations of some given mode. To do this we first need to define our creation operators:

[14]:

from qualg.fock_state import FockOp, FockOpProduct, BaseFockState

[15]:

aw = FockOp('a', 'w')
print(aw)

a+(w)


We have define a creation operator of in mode a and with variable w (e.g. frequency). We can then use this to define the state where this operator acts on the vacuum:

[16]:

s = BaseFockState([aw])
print(s)

a+(w)^1|0>


Lets see what happens when we take inner products of these fock states.

[17]:

aw = FockOp('a', 'w')
av = FockOp('a', 'v')
saw = BaseFockState([aw])
sav = BaseFockState([av])
print(saw.inner_product(sav))

D[w-v]


Hey! That’s pretty cool, we got a delta function with the variables w and v.

Lets look at a more complicated example where we have states which are superpositions over modes defined by a wavepacket. For example let’s say we have one state with the wave-packet phi and the other with psi as follows:

[18]:

from qualg.scalars import SingleVarFunctionScalar

[19]:

phi = SingleVarFunctionScalar("phi", 'w')
psi = SingleVarFunctionScalar("psi", "w")
s_phi = phi * saw.to_state()
s_psi = psi * saw.to_state()
print(s_phi)
print(s_psi)

phi(w)*a+(w)^1|0>
psi(w)*a+(w)^1|0>


Which represents the two states

\begin{align}\begin{aligned}\Phi(\omega)\; a^\dagger(\omega)\;|0\rangle\\and\end{aligned}\end{align}
$\Psi(\omega)\; a^\dagger(\omega)\;|0\rangle$

.

We can now take the inner product of these two states:

[20]:

inp = s_phi.inner_product(s_psi)
print(inp)

phi*(w)*psi(w')*D[w-w']


What is then left to do is to integrate out this expression since there are integrals from the states which we left out until now. By default, the integrate function will integrate out all varibles in the expression if not otherwise specified.

[21]:

from qualg.integrate import integrate

[22]:

integrated_inp = integrate(inp)
print(integrated_inp)

<phi|psi>


What’s this? Looks like an inner-product between functions. That’s exactly what it is. We can see this more clearly by writing out the representation of the state:

[23]:

print(repr(integrated_inp))

InnerProductFunction('phi', 'psi')


So we have found that the inner-product of these two states is exactly the inner-product of the wave-packets describing the states.

## Operators¶

Operators work very much like states in that there is a BaseOperator-class which represents the terms of an Operator. A BaseOperator is described by a left and right BaseState, i.e. $$|\textrm{left}\rangle\langle\textrm{right}|$$.

[24]:

from qualg.operators import BaseOperator, Operator, outer_product


### Single-qubit operators¶

[25]:

# Create a basis for single-qubit states
bs = [BaseQuditState(f"{i}") for i in range(2)]

bp0 = BaseOperator(bs[0], bs[0])
print(bp0)

Op[|0><0|]


We can from a BaseOperator create an Operator as:

[26]:

p0 = bp0.to_operator()
print(p0)

1*Op[|0><0|]


Lets say we want to create the Hadamard operation, we can then make use of the outer_product function:

[27]:

# Create standard and Hadamard basis states
s0 = bs[0].to_state()
s1 = bs[1].to_state()
h0 = (s0 + s1) * (1 / np.sqrt(2))
h1 = (s0 - s1) * (1 / np.sqrt(2))

h = outer_product(h0, s0) + outer_product(h1, s1)
print(h)

0.7071067811865475*Op[|0><0|] + 0.7071067811865475*Op[|1><0|] + 0.7071067811865475*Op[|0><1|] + -0.7071067811865475*Op[|1><1|]


We can now let this operator act on a state:

[28]:

print(h * s0)
print(h * h0)

0.7071067811865475*|0> + 0.7071067811865475*|1>
0.9999999999999998*|0>


### General operators¶

We can also create operators acting on non-qubit states but also operators taking a state in some Hilbert-space to another.

For example let’s define an operator from a qubit to a fock state:

[29]:

# Create standard basis states
bs = [BaseQuditState(f"{i}") for i in range(2)]
s0 = bs[0].to_state()
s1 = bs[1].to_state()

# Create two fock states in different modes
a = FockOp('a', 'w')
b = FockOp('b', 'w')
fs0 = BaseFockState([a]).to_state()
fs1 = BaseFockState([b]).to_state()

# Create an operator from a qubit state to a fock state
op = outer_product(fs0, s0) + outer_product(fs1, s1)
print(op)

1*Op[a+(w)^1|0><0|] + 1*Op[b+(w)^1|0><1|]


## Measuring¶

We can use what we have learned about states and operators to now also measure states given a set of measurement operators. We can create general POVM measurement but we will here for simplicity only perform a measurement in the standard single-qubit basis. However general POVMs work in the same way.

To perform a measuremnt we first need to define the Kraus operators that define the measuremnent:

[30]:

from qualg.measure import measure


To perform a measurement we first need to define the Kraus operators that define the measurement:

[31]:

# Create single-qubit states
s0 = BaseQubitState("0").to_state()
s1 = BaseQubitState("1").to_state()
h0 = (s0 + s1) * (1 / np.sqrt(2))

# Create a projective measurement
P0 = outer_product(s0, s0)
P1 = outer_product(s1, s1)
kraus_ops = {0: P0, 1: P1}


What we pass into the measure-function is a dictionary where the keys are the measurement outcomes/labels and the values the Kraus operators. Note that there is not check that the operators actually form a valid POVM.

Lets now measure some states and see that we get (run the below examples multiple times to see that happens):

[32]:

# Measure |0>
meas_res = measure(s0, kraus_ops)
print(meas_res)

MeasurementResult(outcome=0, probability=1, post_meas_state=State([(BaseQubitState('0'), 1.0)]))

[33]:

# Measure |1>
meas_res = measure(s1, kraus_ops)
print(meas_res)

MeasurementResult(outcome=1, probability=1, post_meas_state=State([(BaseQubitState('1'), 1.0)]))

[34]:

# Measure |+>
meas_res = measure(h0, kraus_ops)
print(meas_res)

MeasurementResult(outcome=0, probability=0.4999999999999999, post_meas_state=State([(BaseQubitState('0'), 1.0)]))