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).
# 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.
import tequila as tq
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 | True | True | True | True cirq | True | True | True | True pyquil | False | False | False | False symbolic | True | False | False | True
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
circuit = tq.gates.H(target=0) + tq.gates.CNOT(target=1,control=0)
Circuits can be printed out in list-type form or can be drawn by the backends.
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'
)
print(circuit)
circuit: H(target=(0,)) X(target=(1,), control=(0,))
tq.draw(circuit)
Lets do the same with a list of qubits and see what happens
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)$
# 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)
Lets simulate our last created circuit.
This can be done by the tq.simulate
function.
The return type is a QubitWaveFunction
wfn = tq.simulate(circuit)
print(wfn)
+0.5000|00> +0.5000|10> +0.5000|01> +0.5000|11>
You can control on which backend the simulation is executed with the backend=name
keyword where name is one of the backends that are installed on your system (see first cell)
# 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>
If you don't want to simulate a full wavefunction but rather simulate individual samples from it you can pass down the samples=integer
keyword
measurements = tq.simulate(circuit, samples=10)
print(measurements)
+4.0000|00> +1.0000|10> +2.0000|01> +3.0000|11>
You can access the individual measurements either by integers in binary notation or by binary strings
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
compiled_circuit = tq.compile(circuit, samples=10, backend="qulacs")
compiled_circuit(samples=10, read_out_qubits=[0])
+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.
# 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>
You can see if a circuit is parametrized by calling extract_variables
from the circuit.
Note that if you use the cirq backend to draw the circuit
print("circuit has variables: ", circuit.extract_variables())
circuit has variables: [a]
Now the same thing a little more sophisticated.
We will parametrized the gate by $a^2$ and also we want the $a$ to be in units of $\pi$
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>
Now the same again with a stronger transformation on the variables:
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.
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>
Lets do the same thing once more, but this time with a customized transformation (which will do the same as in the last cell)
# 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>
Note that you don't have to use strings as variable names.
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
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)$
# 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)
Hamiltonian +1.0000X(0) Objective with 1 unique expectation values total measurements = 1 variables = [a] types = not compiled
# better not use it for large objectives
tq.draw(E)
Objective with 1 unique expectation values total measurements = 1 variables = [a] types = not compiled Expectation Value 0: total measurements = 1 variables = 1 circuit = 5939538847903336889_0.png
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.
variables = {"a": 1.0}
value = tq.simulate(E, variables=variables)
print("Objective({}) = {}".format(variables["a"], value))
Objective(1.0) = 0.8414709848078965
Here are some more intuitive examples of Hamiltonians.
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.
# 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())
+1.0000X(0)X(1)X(2)X(3)+1.0000Y(2)+1.0000Z(0)Z(1)X(2) is hermitian = True +1.4142e^(-0.2500πi)X(0) is hermitian = False hermitian part = +1.0000X(0) anti-hermitian part = -1.0000iX(0) +0.2500+0.2500Z(1)+0.2500Z(0)+0.2500Z(0)Z(1) is hermitian = True +0.5000+0.5000Z(0)Z(1)+0.5000X(0)X(1)-0.5000Y(0)Y(1) is hermitian = True
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.
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
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
# 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} $$# 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.
# 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)
Objective with 1 unique expectation values total measurements = 1 variables = [a] types = not compiled
At last, lets take our last objective but sample from it.
This means that in the evaluation of the expectation value the measurements on the underlying circuit are sampled.
Feel free to play with the number of samples.
# 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)
Objective with 1 unique expectation values total measurements = 1 variables = [a] types = not compiled
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.
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
# 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);
Objective: Objective with 1 unique expectation values total measurements = 1 variables = [a] types = not compiled
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.
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()
This is the gradient objective: Objective with 2 unique expectation values total measurements = 2 variables = [a] types = not compiled
In the next example we will use an initial wavefunction (created by $U_0$) that has complex coefficients, to demonstrate that the assume_real flag does make a difference if the initial wavefunction is not real. The difference is however quite small (which we observe often), and you can think about using the assume_real flag for coarse grained pre-optimization if you are dealing with complex initial wavefunction.
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()
This is the gradient objective for E0 Objective with 2 unique expectation values total measurements = 4 variables = [a] types = not compiled This is the gradient objective for E1 Objective with 4 unique expectation values total measurements = 8 variables = [a] types = not compiled This is the gradient objective for E2 Objective with 8 unique expectation values total measurements = 16 variables = [a] types = not compiled
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) $$# 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);