code
Authors

Gaurav Saxena

Adapted by Elisabeth Welizky

Published

June 21, 2024

This tutorial introduces you to the QTensor class using which you can form vectors, matrices, or tensors whose elements are tequila objectives. This class is derived from numpy.ndarray and therefore, all operations that can be performed on an ndarray object can be performed on a QTensor object.

A short tutorial on using QTensor class

Example

First, let’s take a look at the available simulators:

Code
import tequila as tq
from tequila import numpy
from numpy import pi
tq.show_available_simulators()
backend         | wfn        | sampling   | noise      | installed 
--------------------------------------------------------------------
qulacs_gpu      | False      | False      | False      | False     
qulacs          | True       | True       | True       | True      
qibo            | False      | False      | False      | False     
qiskit          | False      | False      | False      | False     
cirq            | False      | False      | False      | False     
pyquil          | True       | True       | True       | True      
symbolic        | True       | False      | False      | True      
qlm             | False      | False      | False      | False     

We first create some simple Hamiltonians and tequila objectives to be used as elements in forthcoming examples

# Hamiltonians
H = tq.paulis.X(0)
Hz = tq.paulis.Z(0)

# Gates/circuits
U1 = tq.gates.Ry(angle='a',target=0) 
U2 = tq.gates.X(0)+U1
U3 = tq.gates.Ry(angle='b',target=0) 

# Expectation Values
E1 = tq.ExpectationValue(H=H, U=U1)
E2 = tq.ExpectationValue(H=H, U=U2)
E3 = tq.ExpectationValue(H=H, U=U1+U3)
E4 = tq.ExpectationValue(H=H, U=U3)
E5 = tq.ExpectationValue(H=Hz, U=U1)
E6 = tq.ExpectationValue(H=Hz, U = U3)

# variables
variables={'a':numpy.pi/4, 'b':numpy.pi/3} 

Now let us construct QTensors.
First suppose we want an array (of objectives) of length 3. We define a QTensor in the following way:

Code
V1 = tq.QTensor(shape=[3])
V1[0] = E1
V1[1] = E2
V1[2] = E3

In the above example, we first initialized a QTensor of shape (3,1). Then, we assigned a tequila objective to each element of the QTensor.
Another way of initializing a QTensor is by providing it an objective list (using objective_list) and a shape as follows.

Code
V1_ = tq.QTensor(objective_list = [E1,E2,E3], shape=[3])

Caution: The elements of objctive_list must be tequila objectives!

To view the details of the QTensor, we can use print(<QTensor_name>):

Code
print('V1:',V1)
print('\n')
print('V1_:',V1_)
V1: QTensor of shape (3,) with 3 unique expectation values
total measurements = 3
variables          = [a, b]
types              = not compiled


V1_: QTensor of shape (3,) with 3 unique expectation values
total measurements = 3
variables          = [a, b]
types              = not compiled

Similarly, we can create matrices and tensors. Note that it is not necessary that the tensor element is a single expectation value. See the following example:

V2 = tq.QTensor(shape=[2,2])
V2[0,0] = E1
V2[0,1] = E4
V2[1,0] = E5
V2[1,1] = E6 + E5.apply(tq.numpy.square)

We can similarly create tensors. Below we create a (2,2,2) tensor wich has 8 elements and we fill it using 6 different expectation values

Code
V3 = tq.QTensor(shape=[2,2,2])
V3[0,0,0] = E1
V3[0,0,1] = E2
V3[0,1,0] = E3
V3[0,1,1] = E4
V3[1,0,0] = E5
V3[1,0,1] = E6
V3[1,1,0] = E4
V3[1,1,1] = E3 + E4**2
print("V2:\n",V2)
print("\nV3:\n",V3)
V2:
 QTensor of shape (2, 2) with 4 unique expectation values
total measurements = 4
variables          = [a, b]
types              = not compiled

V3:
 QTensor of shape (2, 2, 2) with 6 unique expectation values
total measurements = 6
variables          = [a, b]
types              = not compiled

Compilation and Simulation

We can compile and simulate QTensors in exactly the same way as we compile and simulate objectives.

Code
print(tq.simulate(V1,variables)) 
[ 0.70710678 -0.70710678  0.96592583]
Code
V4 = tq.compile(V2,variables)
print(V4,'\n')
print(V4(variables))
QTensor of shape (2, 2) with 5 unique expectation values
total measurements = 5
variables          = [a, b]
types              = [<class 'tequila.simulators.simulator_qulacs.BackendExpectationValueQulacs'>] 

[[0.70710678 0.8660254 ]
 [0.70710678 1.        ]]

Applying transformations on QTensors

We can apply any operation to the QTensor like we apply to a tequila objective. In the case of QTensors, the function/operation is applied element-wise. For instance

V5 = V1.apply(numpy.exp)
Code
print(V5)
print(tq.simulate(V5,variables))
QTensor of shape (3,) with 3 unique expectation values
total measurements = 3
variables          = [a, b]
types              = not compiled
[2.02811498 0.49306869 2.62721888]
Code
V6 = V2.apply(numpy.sin)

print(repr(V6))

V6compiled  = tq.compile(V6,variables)
print(repr(V6compiled))
print(V6compiled(variables))

# print(tq.simulate(V6,variables))
array([['f([a])', 'f([b])'],
       ['f([a])', 'f([b, a])']], dtype=object)
array([['f([a])', 'f([b])'],
       ['f([a])', 'f([b, a])']], dtype=object)
[[0.64963694 0.76175998]
 [0.64963694 0.84147098]]

You can even define your own function and give it as input:

def my_func(x):
    return 2*x

V7 = V3.apply(my_func)
print(tq.simulate(V7,variables))
[[[ 1.41421356 -1.41421356]
  [ 1.93185165  1.73205081]]

 [[ 1.41421356  1.        ]
  [ 1.73205081  3.43185165]]]

Similarly, we can apply gradient function on QTensor. grad is applied on each element of the QTensor

Code
# print(V6)
dV2da = tq.grad(V2,'a')

print(repr(dV2da))
print(type(dV2da))
print(dV2da)

dV2dab = tq.grad(dV2da,'b')
print(dV2dab)
# compiled_dV2 = tq.compile(dV2da)
# print(compiled_dV2(variables))

print(tq.simulate(dV2dab,variables))
array([['f([a])', 'f([])'],
       ['f([a])', 'f([b, a])']], dtype=object)
<class 'tequila.objective.qtensor.QTensor'>
QTensor of shape (2, 2) with 8 unique expectation values
total measurements = 8
variables          = [a, b]
types              = not compiled
QTensor of shape (2, 2) with 6 unique expectation values
total measurements = 6
variables          = [b, a]
types              = not compiled
[[0. 0.]
 [0. 0.]]

With these QTensors, we can do all the operations that we can with numpy arrays. Some of them are given below:

Code
V8 = V2*V2
V9 = numpy.dot(V1,V1)
V10 = numpy.dot(V2,V2)
V11 = numpy.matmul(V2,V2)

print(tq.simulate(V9,variables))
print("Type(V10): ")
print(type(V10))
1
Dot product of two arrays:
2
Some operations on the results
1.9330127018922196
Type(V10): 
<class 'tequila.objective.qtensor.QTensor'>

However, there is one exception that we found. The tensordot method returns an ndarray rather than a QTensor when acting on QTensor objects. If such an issue occurs, recast as follows:

Code
V12 = numpy.tensordot(V3,V3)

print(list(V12.flatten()))
print(type(V12[0,0]))
[f([a, b]), f([a, b]), f([a, b]), f([a, b])]
<class 'tequila.objective.objective.Objective'>
Code
V13 = tq.QTensor(objective_list = list(V12.flatten()),shape=V12.shape)
print(V13)
print(type(V13[0,1]))
print(tq.simulate(V13,variables ))
QTensor of shape (2, 2) with 6 unique expectation values
total measurements = 6
variables          = [a, b]
types              = not compiled
<class 'tequila.objective.objective.Objective'>
[[1.25       0.85662583]
 [3.08137071 3.31042685]]