Basic Structures#

import tequila as tq
import numpy

In quantum computing you can basically represent everything with Paulistrings - tensor products of Pauli matrices. In tequila a Paulistring \(P\) can be used to initialize Hamiltonians (that will define measurements) $\( H = \sum_k c_k P_k \)\( and quantum gates as \)\( U(a) = e^{-i\frac{a}{2} P} \)$ where the minus and the 1/2 are convention.

Hamiltonians#

Here are some examples on how to initialize Hamiltonians on qubits

P = tq.QubitHamiltonian("X(0)Y(1)") # string based
P = tq.paulis.X(0)*tq.paulis.Y(1) 
print(P)
+1.0000X(0)Y(1)
H = P + tq.paulis.X([0,1]) + 2.0
print(H)
+1.0000X(0)Y(1)+1.0000X(0)X(1)+2.0000

Often useful for demonstrations: You can convert to a full matrix (not recommended for large qubit numbers though).

matrix = H.to_matrix()
print(matrix)
[[2.+0.j 0.+0.j 0.+0.j 1.-1.j]
 [0.+0.j 2.+0.j 1.+1.j 0.+0.j]
 [0.+0.j 1.-1.j 2.+0.j 0.+0.j]
 [1.+1.j 0.+0.j 0.+0.j 2.+0.j]]

Some convenience functions are implemented, like tq.paulis.Projector that realizes the projector

\[ P_\Psi = \lvert \Psi \rangle\langle \Psi \rvert \]

as a sum over Paulistrings. The Wavefunction can be given in the form of a tq.QubitWaveFunction (either returned from a simulation or manually initialized from strings or arrays).

wfn = "1.0|10> + 1.0|01>"
Proj = tq.paulis.Projector(wfn)
print(Proj)
print(Proj*Proj) # wfn was not normalized in this example
+0.5000-0.5000Z(0)Z(1)+0.5000X(0)X(1)+0.5000Y(0)Y(1)
+1.0000-1.0000Z(0)Z(1)+1.0000X(0)X(1)+1.0000Y(0)Y(1)
wfn = tq.QubitWaveFunction("1.0|10> + 1.0|01>").normalize()
Proj = tq.paulis.Projector(wfn)
print(Proj)
print(Proj*Proj)
+0.2500-0.2500Z(0)Z(1)+0.2500X(0)X(1)+0.2500Y(0)Y(1)
+0.2500-0.2500Z(0)Z(1)+0.2500X(0)X(1)+0.2500Y(0)Y(1)
# array based
v,vv = numpy.linalg.eigh(H.to_matrix())
wfn = tq.QubitWaveFunction(vv[:,0])
Proj = tq.paulis.Projector(wfn)
print(Proj)
print(Proj*Proj)
+0.2500+0.2500Z(0)Z(1)-0.1768X(0)X(1)-0.1768X(0)Y(1)-0.1768Y(0)X(1)+0.1768Y(0)Y(1)
+0.2500+0.2500Z(0)Z(1)-0.1768X(0)X(1)-0.1768X(0)Y(1)-0.1768Y(0)X(1)+0.1768Y(0)Y(1)+0.0000iZ(1)+0.0000iZ(0)

Circuits#

Circuits can be assembled from primitive Pauli-rotations. All gates can be controlled with the keyword control=[list of qubits] and parametrized with the keyword angle=number/abstract-tequila-object.

U = tq.gates.Rp(paulistring="X(0)Y(1)", angle=1.0)
U+= tq.gates.Rp(paulistring="Y(0)X(1)", angle=1.0)

wfn = tq.simulate(U)
print(U)
print("U|00> = ", wfn)
circuit: 
Exp-Pauli(target=(0, 1), control=(), parameter=1.0, paulistring=X(0)Y(1))
Exp-Pauli(target=(0, 1), control=(), parameter=1.0, paulistring=Y(0)X(1))

U|00> =  +0.5403|00> +0.8415|11> 

More on circuits#

If you prefer typical gates, you can compile the circuits.
More information on tequila circuits can be found here: https://jakobkottmann.com/posts/tq-circuits/

U = tq.compile_circuit(U)
print(U)
tq.draw(U) # uses cirq (change with backend=...)
circuit: 
Ry(target=(0,), parameter=-1.5707963267948966)
Rx(target=(1,), parameter=1.5707963267948966)
X(target=(1,), control=(0,))
Rz(target=(1,), parameter=1.0)
X(target=(1,), control=(0,))
Ry(target=(0,), parameter=1.5707963267948966)
Rx(target=(1,), parameter=-1.5707963267948966)
Rx(target=(0,), parameter=1.5707963267948966)
Ry(target=(1,), parameter=-1.5707963267948966)
X(target=(1,), control=(0,))
Rz(target=(1,), parameter=1.0)
X(target=(1,), control=(0,))
Rx(target=(0,), parameter=-1.5707963267948966)
Ry(target=(1,), parameter=1.5707963267948966)

0: ───Y^-0.5───@─────────────@───Y^0.5────X^0.5────@─────────────@───X^-0.5───
               │             │                     │             │
1: ───X^0.5────X───Z^0.318───X───X^-0.5───Y^-0.5───X───Z^0.318───X───Y^0.5────
''
# useful
tq.gates.CNOT(0,1)
tq.gates.X(0, control=1) # same as above
tq.gates.Toffoli(0,1,2)
tq.gates.X(0, control=[1,2]) # same as Tofolli
tq.gates.SWAP(0,1)
tq.gates.H(0)
circuit: 
H(target=(0,))

Wavefunctions#

The tq.QubitWaveFunction is a primitive class that is convenient for some things, but it is not how tequila is supposed to be used (as we cannot access wavefunctions directly). Here are however a few features that sometimes come in handy when debugging or illustrating concepts

# initialization
wfn = tq.QubitWaveFunction("1.0|00> + 1.0|11>")
wfn = tq.QubitWaveFunction([1.0,0.0,0.0,1.0])
# inner products
c=wfn.inner(wfn)
# normalization
wfn=wfn.normalize()
# apply Paulistrings (does not work with circuits -- could be realized though)
op = tq.paulis.X(0)
wfn2 = op(wfn)

Exercise#

As it is not the main focus of tequila, we currently don’t have a U.to_matrix() function for a quantum circuit. For circuits consisting of Pauli-Gates, this can however be realized via the following formula

\[ e^{-i\frac{a}{2}P} = \cos\left(\frac{a}{2}\right) -i \left(\frac{a}{2}\right)P \]

Try to write a function that converts a tequila circuit into a matrix. Assume that all gates are Pauli-Gates.

# example

U = tq.gates.Rp(paulistring="X(0)", angle=numpy.pi)

M = 0.0
for gate in U.gates:
    G = gate.make_generator()
    a = gate.parameter
    m = tq.numpy.cos(a/2) - 1.0j*tq.numpy.sin(a/2)*G
    M += m
    
print(M)
-1.0000iX(0)

For Experts:#

# if you want to be rigorous, you can check if the generator is a single Pauli string
H1 = tq.paulis.X(0)*tq.paulis.Y(1)
H2 = tq.paulis.Z(0)*tq.paulis.Y(1)
H3 = H1 + H2
print(H1, " ", len(H1), " primitives")
print(H2, " ", len(H2), " primitives")
print(H1 + 1.0, " ", len(H1), " primitives") # will ignore global phases
print(H3, " ", len(H3), " primitives")

# and assure with gate.is_controlled() that there are no control qubits on the gate
# with `make_generator(include_control_qubits=True)` you can get the generator for the controlled gate
# which allows you to decompose the controlled-Pauli-Gate into a sequence of two primitive Pauli Gates
+1.0000X(0)Y(1)   1  primitives
+1.0000Z(0)Y(1)   1  primitives
+1.0000X(0)Y(1)+1.0000   1  primitives
+1.0000X(0)Y(1)+1.0000Z(0)Y(1)   2  primitives