#!/usr/bin/env python # coding: utf-8 # # Tequila Basic Usage # ## Install Tequila # # 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 get_ipython().system('{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) get_ipython().system('{sys.executable} -m pip install --upgrade qulacs') if i_want_the_development_version: get_ipython().system('{sys.executable} -m pip install git+https://github.com/aspuru-guzik-group/tequila.git@devel') else: # install basic tequila get_ipython().system('{sys.executable} -m pip install tequila-basic') # install a simulator (replace qulacs with something else (qibo, cirq, qiskit, pyquil) if you have trouble) get_ipython().system('{sys.executable} -m pip install --upgrade qulacs') # ## Create and Simulate Quantum Circuits # # 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() # ### Create Simple Circuits # 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) # 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'`) # In[4]: print(circuit) # 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) # ### Simulate and Sample Simple Circuits # Lets simulate our last created circuit. # This can be done by the `tq.simulate` function. # The return type is a `QubitWaveFunction` # In[8]: wfn = tq.simulate(circuit) print(wfn) # 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) # 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) # 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 # In[10]: measurements = tq.simulate(circuit, samples=10) print(measurements) # You can access the individual measurements either by integers in binary notation or by binary strings # In[11]: print(measurements(0)) print(measurements("00")) print(measurements(2)) print(measurements("10")) # 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]) # ### Create Parametrized Circuits # # 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) # 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 # In[14]: print("circuit has variables: ", circuit.extract_variables()) # 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$ # 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) # 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. # 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) # Lets do the same thing once more, but this time with a customized transformation (which will do the same as in the last cell) # 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) # 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 # 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()) # ## Create and Simulate Objectives/Expectationvalues # 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: # # $$ # E = \langle\Psi(a)\rvert H \lvert\Psi(a) \rangle = \langle0\rvert U^\dagger\left(a\right) H U\left(a\right) \lvert 0\rangle = \langle H \rangle_{ U\left(a\right)} # $$ # # 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)) # 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. # 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)) # 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 # # $$ # L = E^2 + 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) # 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. # 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 # # 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); # 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); # ### Details on Derivatives # # Gates that are not directly differentiable by the original [shift-rule](https://arxiv.org/abs/1811.11184) are automatically decomposed into a product of gates that is. This process happens automatically and does not need further specifications. See the [tequila-paper](https://arxiv.org/pdf/2011.03057.pdf) 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](https://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](https://doi.org/10.1039/D0SC06627C) for an explanation as well as [arxiv.org/abs/2104.05695](https://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 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. # 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() # # All in one # # 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);