You can install tequila with minimal dependencies over PyPi via `pip install tequila-basic`

.

It is advised to also install a quantum backend (that can act as simulator or interface to hardware).

The recommended backend for simulation is Qulacs and with Qiskit you have access to IBM's cloud computers.

For some operating systems (windows) installing qulacs can become a bit challenging.

In this case: Just install another backend (see the cells below on an overview of what is supported).

In [1]:

```
# If you execute this cell it will install tequila
# we however recommend to install it by yourself and not over jupyter
do_install = False # set to True if you want to install from jupyter
i_am_using_windows = False # set to true if you can not install Jax/Jaxlib, necessary in windows OS
i_want_the_development_version = False
if do_install:
import sys
if i_am_using_windows:
# install
!{sys.executable} -m pip install git+https://github.com/aspuru-guzik-group/tequila.git@windows
# install a simulator (replace qulacs with something else (qibo, cirq, qiskit, pyquil) if you have trouble)
# in order to install it on windows you need c++ compilers (install for example over VisualStudio)
!{sys.executable} -m pip install --upgrade qulacs
if i_want_the_development_version:
!{sys.executable} -m pip install git+https://github.com/aspuru-guzik-group/tequila.git@devel
else:
# install basic tequila
!{sys.executable} -m pip install tequila-basic
# install a simulator (replace qulacs with something else (qibo, cirq, qiskit, pyquil) if you have trouble)
!{sys.executable} -m pip install --upgrade qulacs
```

Tequila operators on abstract data types which can be translated and executed on various backends.

In this part of the tutorial we show how to initialize and execute those.

First import tequila and check which backends are installed on your system.

If supported backends are not installed you can for most of them just install them with
`pip install --upgrade name`

in your command line

If you have old versions of supported backends installed that might get you in trouble.

You can upgrade them with the same command.

In [2]:

```
import tequila as tq
import numpy
from numpy import pi
tq.show_available_simulators()
```

Lets create some simple unparametrized circutis.

With unparametrized we mean here, that possible angles in the circuits are fixed.

You can create tequila circuits and gates over the `tq.gates`

module.

Gates and circuits can be glued together with the `+`

operation.

Gates can receive the keyword arguments `target`

and `control`

which defines qubit(s) on which the gates act

In [3]:

```
circuit = tq.gates.H(target=0) + tq.gates.CNOT(target=1,control=0)
```

The result of tq.draw will depend which backends you have installed.

If you want to draw with a specific backend, just pass the `backend`

keyword, e.g. tq.draw(circuit,`backend='cirq'`

)

In [4]:

```
print(circuit)
```

circuit: H(target=(0,)) X(target=(1,), control=(0,))

In [5]:

```
tq.draw(circuit)
```

Lets do the same with a list of qubits and see what happens

In [6]:

```
circuit = tq.gates.H(target=[0,1]) + tq.gates.X(target=1, control=0)
tq.draw(circuit)
```

Some gates have to be parametrized by `angle`

and some can be parametrized by `power`

.

Note that if you use cirq to draw circuits, it will display those in different units. This does not affect the simulation.

Tequila uses the standard convention for qubit rotations: $$ R_i(\theta) = e^{-i\frac{\theta}{2} \sigma_i}, \qquad i \in \left\{ X, Y, Z \right\} $$

which is carried over for multi-qubit rotations $$ R_P(\theta) = e^{-i\frac{\theta}{2} P} $$ where $P$ is a paulistring like e.g. $X(0)Y(1)$

In [7]:

```
# Some further examples
circuit0 = tq.gates.Ry(angle=1.0, target=0) + tq.gates.X(target=1, control=0)
circuit1 = tq.gates.Y(power=0.5, target=0) + tq.gates.Ry(angle=1.0, target=1, control=0)
circuit2 = tq.gates.Rp(angle=1.0, paulistring="Y(0)") + tq.gates.X(target=1, control=0) # acts the same as circuit0
circuit3 = tq.gates.Rp(angle=1.0, paulistring="X(0)Y(1)")
generator = tq.paulis.Y(0)
circuit4 = tq.gates.Trotterized(generators=[generator], angles=[1.0], steps=1) # acts the same as circuit0
generator = tq.paulis.X(0)*tq.paulis.Y(1)
circuit5 = tq.gates.Trotterized(generators=[generator], angles=[1.0], steps=1) # acts the same as circuit3
tq.draw(circuit3)
```

This can be done by the `tq.simulate`

function.

The return type is a `QubitWaveFunction`

In [8]:

```
wfn = tq.simulate(circuit)
print(wfn)
```

+0.5000|00> +0.5000|10> +0.5000|01> +0.5000|11>

`backend=name`

keyword where name is one of the backends that are installed on your system (see first cell)

In [9]:

```
# simulate on 'qulacs' backend (which is the default)
# Note that this cell will crash if qulacs is not installed
# just switch the name with something that is installed on your system (check the first cell)
wfn = tq.simulate(circuit, backend='qulacs')
print(wfn)
```

+0.5000|00> +0.5000|10> +0.5000|01> +0.5000|11>

`samples=integer`

keyword

In [10]:

```
measurements = tq.simulate(circuit, samples=10)
print(measurements)
```

+4.0000|00> +1.0000|10> +2.0000|01> +3.0000|11>

In [11]:

```
print(measurements(0))
print(measurements("00"))
print(measurements(2))
print(measurements("10"))
```

4 4 1 1

Individual measurement instructions can be added over the `read_out_qubits`

keyword

In [12]:

```
compiled_circuit = tq.compile(circuit, samples=10, backend="qulacs")
compiled_circuit(samples=10, read_out_qubits=[0])
```

Out[12]:

+5.0000|0> +5.0000|1>

Now we will explore how to create parametrized circuits.

This works analogue to the simple circuits before, just that `angle`

and `power`

can be set to hashable types.

You can either initialize the variable directly or you can create a tequila variable object and use that.

The latter is only important if you intend to pass down a manipulated variable (like for example $a^2$ instead of just $a$)

Lets start with something simple: A parametrized rotation on a single qubit.

We will call the variable $a$.

If the circuit gets simulated the value of the variable has to be specified.

This is done by passing down a dictionary holding the names and values of all variables.

In [13]:

```
# initialize the parametrized circuit
circuit = tq.gates.Ry(angle="a", target=0)
pi = tq.numpy.pi
# set the value we want to simulate
variables = {"a" : pi**2}
wfn = tq.simulate(circuit, variables=variables)
print(wfn)
```

+0.2206|0> -0.9754|1>

`extract_variables`

from the circuit.

Note that if you use the cirq backend to draw the circuit

In [14]:

```
print("circuit has variables: ", circuit.extract_variables())
```

circuit has variables: [a]

We will parametrized the gate by $a^2$ and also we want the $a$ to be in units of $\pi$

In [15]:

```
a = tq.Variable("a")
circuit = tq.gates.Ry(angle=(a*pi)**2, target=0)
# set the value we want to simulate
variables = {"a" : 1.0}
wfn = tq.simulate(circuit, variables=variables)
print(wfn)
```

+0.2206|0> -0.9754|1>

In general, if you want to apply transformations on Variables (and later on Objectives) this can be done with

`variable.apply(your_function)`

.

Here we will take the exponential function of numpy. Note that it is adviced to take those functions from `tq.numpy`

since this will be the `jax`

numpy used for automaticl differentiation.

By doing so consistently you will avoid potential problems with gradients later.

In [16]:

```
a = tq.Variable("a")
circuit = tq.gates.Ry(angle=((a*pi)**2).apply(tq.numpy.exp), target=0)
# set the value we want to simulate
variables = {"a" : 1.0}
wfn = tq.simulate(circuit, variables=variables)
print(wfn)
```

-0.9856|0> -0.1692|1>

In [17]:

```
# define your own transformation
def my_trafo(x):
return tq.numpy.exp(x**2)
a = tq.Variable("a")
# we will put the variable manipulation here for more overview
a = a*pi # a is now in a*pi
a = a.apply(my_trafo) # a is now exp((a*pi)**2)
circuit = tq.gates.Ry(angle=a, target=0)
# set the value we want to simulate
variables = {"a" : 1.0}
wfn = tq.simulate(circuit, variables=variables)
print(wfn)
```

-0.9856|0> -0.1692|1>

You can use any hashable type except numeric types since those will be interpeted as fixed numbers.
Here is one example using a combination of strings and tuples

In [18]:

```
circuit = tq.gates.Ry(angle=(1,"a", "its a stupid example"), target=0)
print(circuit.extract_variables())
circuit = tq.gates.Ry(angle=(1,2,3), target=0)
print(circuit.extract_variables())
```

[(1, 'a', 'its a stupid example')] [(1, 2, 3)]

Within tequila you can define qubit operators which can either be used to generate gates and circuits over `tq.gates.Rp`

, `tq.gates.Trotterized`

or `tq.gates.GeneralizedRotation`

or as Hamiltonians defining the measurements on the quantum experiments.

`QCircuit`

and `QubitHamiltonian`

objects can be combined to expectation values which can be combined and transformed to become more general `Objectives`

(in tequila an expectation value is already objective in its simplest form).

We will start by demonstrating this with a simple one qubit example.

We will take the one qubit rotation gate from previous sections and use a simple pauli operator as hamiltonian.
So our expectation value will be:

where $H = \sigma_x = X $ and $ U\left(a\right) = Ry(a)$

In [19]:

```
# the circuit
U = tq.gates.Ry(angle="a", target=0)
# the Hamiltonian
H = tq.paulis.X(0)
# the Objective (a single expectation value)
E = tq.ExpectationValue(H=H, U=U)
print("Hamiltonian ", H)
print(E)
```

In [20]:

```
# better not use it for large objectives
tq.draw(E)
```

Lets simulate the objective for some choices of our variable.

Note that the simulate function is the same as before for the circuits, you can use the `backend`

and `sample`

key in the same way.

Since the objective is defined with parametrized quantum circuits, the values of the variables have to be passed down in the same way as before.

Note that not all expectationvalues in the objective need to be parametrized and that the parameters don't need to be the same.

In [21]:

```
variables = {"a": 1.0}
value = tq.simulate(E, variables=variables)
print("Objective({}) = {}".format(variables["a"], value))
```

Objective(1.0) = 0.8414709848078965

Note that those operators are not automatically hermitian.

You can use the `split`

function to get the hermitian and/or antihermitian part

Check also the `FAQ`

notebook for some more information.

In [22]:

```
# Pauli Operators can be initialilzed and added/multipled
H = tq.paulis.X(qubit=[0,1,2,3]) + tq.paulis.Y(2) + tq.paulis.Z(qubit=[0,1])*tq.paulis.X(2)
print(H, " is hermitian = ", H.is_hermitian())
H = tq.paulis.Z(0)*tq.paulis.Y(0) + tq.paulis.X(0)
print(H, " is hermitian = ", H.is_hermitian())
hermitian_part, anti_hermitian_part = H.split()
print("hermitian part = ", hermitian_part)
print("anti-hermitian part = ", anti_hermitian_part)
H = tq.paulis.Projector("|00>")
print(H, " is hermitian = ", H.is_hermitian())
H = tq.paulis.Projector("1.0*|00> + 1.0*|11>")
print(H, " is hermitian = ", H.is_hermitian())
```

Lets proceed with our previous simple example of a single qubit rotation and a single X as Hamiltonian.

If you intent to evaluate the objective with lots of different choices of the variables it is useful to compile it. A compiled objective is tied to a specific backend and can be used like a function taking a dictionary of variables.

The compile function takes also the `backend`

and `sample`

keyword. If no backend is chosen tequila will pick automatically from the installed ones. If you intent to sample your objective instead of fully simulate it you can give a dummy integer of samples to `compile`

. This will help tequila pick the best available backend but will not fix the number of samples for future evaluations.

Compiling first will make your code faster since then the abstract circuits do not need to be re-translated to the backend every time.

In [23]:

```
compiled_objective = tq.compile(E)
# the compiled objective can now be used like a function
for value in [0.0, 0.5, 1.0]:
evaluated = compiled_objective(variables={"a": value})
print("objective({}) = {}".format(value, evaluated))
```

objective(0.0) = 0.0 objective(0.5) = 0.479425538604203 objective(1.0) = 0.8414709848078965

Lets simulate our compiled objetive and plot the results

In [24]:

```
def compile_and_evaluate(objective, steps=25, samples=None, start=0, stop=2*pi):
from matplotlib import pyplot as plt
plt.figure()
compiled_objective = tq.compile(objective, samples=samples)
values = [start + (stop-start)*step/steps for step in range(steps)]
evaluated = [compiled_objective(variables={"a": value}, samples=samples) for value in values]
plt.plot(values, evaluated)
plt.show()
return values, evaluated
compile_and_evaluate(E);
```

Lets do the same again with a more complicated objective.

Lets take the square of our original expectation value and shift it by 1

In [25]:

```
# this is the new objective
L = E**2 + 1
compile_and_evaluate(L);
```

And the same, but shifted by $e^{-a^2}$, so the shift is not constant and will only affect $L$ for small values of $a$.

$$ L = E^2 + e^{-a^2} $$In [26]:

```
# For completeness we initialize the variable again here
a = tq.Variable("a")
# to be sure that the variable is the same as the one from the objective we could also do
# a = objective.extract_variables()[0]
# this is the new objective
L = E**2 + (-a**2).apply(tq.numpy.exp)
compile_and_evaluate(L);
```

Now we also transform the expectation value in a more complicated way. It works the same way as it works for variables

$$ L = exp(-E^2) + exp(-a^2)E $$Note that the original expectation value now enters two times into the objective.

But tequila will only evaluate it once per run.

In [27]:

```
# For completeness we initialize the variable again here
a = tq.Variable("a")
# to be sure that the variable is the same as the one from the objective we could also do
# a = objective.extract_variables()[0]
# this is the new objective
L = E**2 + (-a**2).apply(tq.numpy.exp)*E
compile_and_evaluate(L);
# check how many (unique) expectation values are in the objective
print(L)
```

Feel free to play with the number of samples.

In [28]:

```
# For completeness we initialize the variable again here
a = tq.Variable("a")
# to be sure that the variable is the same as the one from the objective we could also do
# a = objective.extract_variables()[0]
# this is the new objective
L = E**2 + (-a**2).apply(tq.numpy.exp)*E
compile_and_evaluate(L, samples=1000);
# check how many (unique) expectation values are in the objective
print(L)
```

Derivatives of objectives are objectives themselves.
They can be simply created by applying `tq.grad`

on a objective.

Lets take first objective and plot its derivative. The first is easy to check since the derivative should just be the shifted sinus curve. Feel free to change the objective and play around.

In [29]:

```
L = E
dLda = tq.grad(L, "a")
d2Ld2a = tq.grad(dLda, "a")
print("Objective:\n", L)
compile_and_evaluate(L);
print("First Derivative:\n",dLda)
compile_and_evaluate(dLda);
print("Second Derivative:\n",d2Ld2a)
compile_and_evaluate(d2Ld2a);
```

Objective: Objective with 1 unique expectation values total measurements = 1 variables = [a] types = not compiled

First Derivative: Objective with 2 unique expectation values total measurements = 2 variables = [a] types = not compiled

Second Derivative: Objective with 6 unique expectation values total measurements = 6 variables = [a] types = not compiled

In [30]:

```
# another example
L = E**2 + (-a**2).apply(tq.numpy.exp)*E
dLda = tq.grad(L, "a")
d2Ld2a = tq.grad(dLda, "a")
print("Objective:\n", L)
compile_and_evaluate(L);
print("First Derivative:\n",dLda)
compile_and_evaluate(dLda);
print("Second Derivative:\n",d2Ld2a)
compile_and_evaluate(d2Ld2a);
```

First Derivative: Objective with 3 unique expectation values total measurements = 3 variables = [a] types = not compiled

Second Derivative: Objective with 9 unique expectation values total measurements = 9 variables = [a] types = not compiled

Gates that are not directly differentiable by the original shift-rule are automatically decomposed into a product of gates that is. This process happens automatically and does not need further specifications. See the tequila-paper for more details.

Gradients of some gates, such as (multi-)controlled-Rotations and QubitExcitations can be further simplified if the wavefunction prior (in the examples below, the wavefunctions prepared by $U_0$) only has real coefficients. This allows to compile the gradients with only a cost-factor of two (see doi.org/10.1039/D0SC06627C for the details and note that (multi-)controlled-Rotations follow the same principles as the fermionic excitations). The `assume_real`

keyword will trigger this improved compilation for controlled-rotations as well as QubitExcitations and FermionicExcitations (see chemistry tutorials for the latter). If the keyword is set False, the associated cost factor will be 4. See again doi.org/10.1039/D0SC06627C for an explanation as well as arxiv.org/abs/2104.05695 for an improved implementation that does not require additional gates - the so-called 4-point rule that is by default used in tequila with version > 1.5.1 for assume_real=False (courtesy of D.Wierichs).

Here is a small example that uses a real initial wavefunction before a double-controlled rotation gate is differentiated: Note that the gradient objective has 2 unique expectation values to evaluate.

In [31]:

```
U0 = tq.gates.H(1)+tq.gates.X(2)
U1 = tq.gates.Ry(target=0, control=[1,2], angle="a", assume_real=True)
H = tq.paulis.X(0)
E = tq.ExpectationValue(H=H, U=U0+U1)
dE = tq.grad(E, "a")
print("This is the gradient objective:\n", dE)
fE = tq.compile(E)
fdE = tq.compile(dE)
values_E = [fE({"a":v}) for v in numpy.linspace(0.0, 2.0*pi,25)]
values_dE = [fdE({"a":v}) for v in numpy.linspace(0.0, 2.0*pi,25)]
from matplotlib import pyplot as plt
plt.plot(values_E)
plt.plot(values_dE)
plt.show()
```

In [32]:

```
U0 = tq.gates.Rx(angle=1.0, target=0)+tq.gates.Rx(angle=2.0, target=1)+tq.gates.Rx(angle=3.0, target=2)
# this will use the techniques of the fermionic-shift for real wavefunctions
U1 = tq.gates.Ry(target=0, control=[1,2], angle="a", assume_real=True)
# this will use the 4-point rule
U2 = tq.gates.Ry(target=0, control=[1,2], angle="a", assume_real=False)
# this will do standard decomposition without any advanced techniques
# Qm = 0.5(1-Z) = |1><1| which corresponds to the "control-generator"
# the gate is equivalent to the others, tequila just has less information about it
U3 = tq.gates.Trotterized(generator=tq.paulis.Y(0)*tq.paulis.Qm([1,2]), angle="a")
H = tq.paulis.X(0)*tq.paulis.Z(1)*tq.paulis.Z(2) + tq.paulis.Y(0)*tq.paulis.X([1,2])
E0 = tq.ExpectationValue(H=H, U=U0+U1)
E1 = tq.ExpectationValue(H=H, U=U0+U2)
E2 = tq.ExpectationValue(H=H, U=U0+U3)
for i,E in enumerate([E0, E1, E2]):
dE = tq.grad(E, "a")
print("This is the gradient objective for E{}".format(i))
print(dE)
fE = tq.compile(E)
fdE = tq.compile(dE)
values_E = [fE({"a":v}) for v in numpy.linspace(0.0, 2.0*pi,25)]
values_dE = [fdE({"a":v}) for v in numpy.linspace(0.0, 2.0*pi,25)]
values_dE2 = [fdE({"a":v}) for v in numpy.linspace(0.0, 2.0*pi,25)]
from matplotlib import pyplot as plt
plt.plot(values_E, label="E{}".format(i))
plt.plot(values_dE, label="dE{}".format(i))
plt.legend()
plt.show()
```

At last we will create a small toy objective.

As expectation value we will use an entangled circuit with one CNOT gate and one Ry rotation and an arbitrary chosen Hamiltonian.

Our objective is defined as

$$\displaystyle L = \langle H \rangle_{U(a)} + e^{-\left(\frac{\partial}{\partial a} \langle H \rangle_{U_{a}}\right)^2 } $$with

$$ H = -X(0)X(1) + \frac{1}{2}Z(0) + Y(1) $$and

$$ U = e^{-\frac{e^{-a^2}}{2} Y(0)} \text{CNOT}(0,1) $$In [33]:

```
# All in one
a = tq.Variable("a")
U = tq.gates.Ry(angle=(-a**2).apply(tq.numpy.exp)*pi, target=0)
U += tq.gates.X(target=1, control=0)
H = tq.QubitHamiltonian.from_string("-1.0*X(0)X(1)+0.5Z(0)+Y(1)")
E = tq.ExpectationValue(H=H, U=U)
dE = tq.grad(E, "a")
objective = E + (-dE**2).apply(tq.numpy.exp)
param, values = compile_and_evaluate(objective, steps=100, start =-5, stop=5);
```