FAQ

code
Author

Elisabeth Welizky

Published

August 2, 2024

Frequently Asked Questions

Which optimization methods can I use?

tq.show_available_optimizers shows you all installed modules on your systems and the methods which tq.minimize understands. Method names are not case sensitive when passed to tq.minimize.

In the end you see which modules are supported and which of them are actually installed on your system.
The table with methods and modules will only show you the methods for modules that are currently installed within your environment.

Of course you can also use tequila objectives for your own optimizers.
You don’t need to use the modules here.

tq.show_available_optimizers()
available methods for optimizer modules found on your system:
method               | optimizer module
--------------------------
NELDER-MEAD          | scipy
COBYLA               | scipy
POWELL               | scipy
SLSQP                | scipy
L-BFGS-B             | scipy
BFGS                 | scipy
CG                   | scipy
TNC                  | scipy
TRUST-KRYLOV         | scipy
NEWTON-CG            | scipy
DOGLEG               | scipy
TRUST-NCG            | scipy
TRUST-EXACT          | scipy
TRUST-CONSTR         | scipy
adam                 | gd
adagrad              | gd
adamax               | gd
nadam                | gd
sgd                  | gd
momentum             | gd
nesterov             | gd
rmsprop              | gd
rmsprop-nesterov     | gd
spsa                 | gd
gpyopt-lbfgs         | gpyopt
gpyopt-direct        | gpyopt
gpyopt-cma           | gpyopt
Supported optimizer modules:  ['scipy', 'gpyopt', 'gd']
Installed optimizer modules:  ['scipy', 'gd', 'gpyopt']

Which simulators/Quantum-Backends can I use?

tq.show_available_simulators shows all simulators/quantum backends which are supported by tequila as well as which are installed within your current environment.

The default choice if you don’t specify a backend when for example simulating a tequila objective with tq.simulate is the first entry of the supported backends which is installed on your system.

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     

Can I avoid re-translation/compilation on my objectives/circuits?

Yes, you can. By calling tq.compile instead of tq.simulate. This will give you back a callable objective.
Check also the basic usage tutorial notebook

U = tq.gates.H(target=1) + tq.gates.Rx(angle="a", target=0, control=1)

# simulate the wavefunction with different variables
wfn0 = tq.simulate(U, variables={"a": 1.0})
wfn1 = tq.simulate(U, variables={"a": 2.0})

print(wfn0)
print(wfn1)

# the same, but avoiding re-compilation
# Note that your compiled object is translated to a quantum backend
# if the backend was not set, tequila it will pick the default which depends
# on which backends you have installed. You will seee it in the printout of the
# compiled circuits
compiled_U = tq.compile(U)
wfn0 = compiled_U(variables={"a":1.0})
wfn1 = compiled_U(variables={"a":2.0})

print("compiled circuit:", compiled_U)
print(wfn0)
print(wfn1)


# With Objectives it works in the same way
H = tq.paulis.Y(0)
E = tq.ExpectationValue(H=H, U=U)
objective = E**2 + 1.0

# simulate the objective with different variables
result0 = tq.simulate(objective, variables={"a": 1.0})
result1 = tq.simulate(objective, variables={"a": 2.0})

print("compiled objective:", objective)
print(result0)
print(result1)

# compile and then simulate
compiled_objective = tq.compile(objective)
result0 = compiled_objective(variables={"a":1.0})
result1 = compiled_objective(variables={"a":2.0})

print("compiled objective:", compiled_objective)
print(result0)
print(result1)
+0.7071|00> +0.6205|01> -0.3390i|11> 
+0.7071|00> +0.3821|01> -0.5950i|11> 
compiled circuit: <tequila.simulators.simulator_qulacs.BackendCircuitQulacs object at 0x3075b72e0>
+0.7071|00> +0.6205|01> -0.3390i|11> 
+0.7071|00> +0.3821|01> -0.5950i|11> 
compiled objective: Objective with 1 unique expectation values
total measurements = 1
variables          = [a]
types              = not compiled
1.1770183545683928
1.2067054526079515
compiled objective: Objective with 1 unique expectation values
total measurements = 1
variables          = [a]
types              = [<class 'tequila.simulators.simulator_qulacs.BackendExpectationValueQulacs'>]
1.1770183545683928
1.2067054526079515

How can I emulate a real quantum computer?

Emulation is performed similarly to running on real devices. All you need to do is pass down the right string to the ‘device’ keyword. For qiskit, these are the same as for regular backends, but have ‘fake_’ at the beginning; I.E to emulate ‘armonk;, set device="fake_armonk". For PyQuil, this is done by adding’-qvm’ to the end of the chosen string, i.e, ‘Aspen-8’ becomes device=Aspen-8-qvm'. For Cirq, only emulation is currently possible; the only string options for cirq are ‘foxtail’,‘bristlecone’,‘sycamore’, and ‘sycamore23’.

When emulating, a few things about the real device will be mimicked, principally its native gate set and its connectivities. Emulation will NOT include noisy emulation by default; If you want to emulate noise, pass down the keyword noise='device'. Using this option without specifying a device will result in an error.

Below, we will emulate pyquil’s Aspen 8, with emulated noise. You need pyquil installed for this to work.

additionally: when real backends cannot be accessed, emulation will be attempted, with a warning.

U = tq.gates.Ry(angle="a", target=0)
H = tq.paulis.X(0)
E = tq.ExpectationValue(H=H, U=U)

# simulate the square of the expectation value with a specific set of variables
result = tq.simulate(E**2, variables={"a":1.0}, samples=1000, backend="pyquil")
print('sampling from pyquil yielded: ', result)
result = tq.simulate(E**2, variables={"a":1.0}, samples=1000, backend="pyquil",device='Aspen-8-qvm')
print('sampling from pyquil while emulating Aspen-8 yielded: ', result)
result = tq.optimizer_scipy.minimize(E**2, initial_values={"a":1.0}, samples=1000,
                                    backend='pyquil', device="Aspen-8-qvm", 
                                    noise='device')
print('optimizing while emulating Aspen-8 with noise yielded a best energy of: ', result.energy)
QVMError: Could not communicate with QVM at http://127.0.0.1:5000

Can I compile Objectives into different backends?

Yes you can. Tequila will print a warning if this happens. Warnings can be ignored by filtering them out (see the python warnings documentation)

If a compiled circuit is used as input to compile then tequila will re-compile the circuit to the new backend (it it differs from the previous one)

If a compiled objective is used as input to compile then tequila will only compile non-compiled expectationvalues into the different backend. Already compiled expectation values will remain untouched

Note that you need at least two different backends for the following cell to execute.
Just change the key to whatever you have installed.

backend1 = "qulacs"
backend2 = "cirq"

U = tq.gates.X(target=[0,1])
print("Example Circuit: ", U)
compiled_1 = tq.compile(U, backend=backend1)
compiled_2 = tq.compile(compiled_1, backend=backend2)
print("Circuit compiled to {} -> ".format(backend1), compiled_1)
print("Circuit compiled to {} -> ".format(backend1), compiled_1)

H = tq.paulis.X(0)*tq.paulis.Y(1) + tq.paulis.X([0,1])
print("\nmake objective with H = ", H)
objective = tq.ExpectationValue(H=H, U=U)
compiled_1 = tq.compile(objective, backend=backend1)

print("\nExpectationValues of objective 1:")
print(compiled_1)
    
objective2 = compiled_1 + objective # Its recommended to avoid those hybrids, but in principle it works

print("\nExpectationValues of partly compiled objective:")
print(objective2)
    
compiled_2 = tq.compile(objective2, backend=backend2)
print("\nExpectationValues of hybdrid compiled objective:")
print(compiled_2)

How do I transform Measurements into Hamiltonians?

We can not answer this question in general, but we can try to give a small example here.

Assume you have a quantum circuit with \(4\) Qubits and you are measuring Qubit \(0\) and \(2\). You define your cost function in the following way:

\[ L(AB) = A + B, \qquad A,B \in \left\{ 0,1 \right\} \]

meaning you accumulate the number of ones measured in your circuit.

The corresponding expectationvalue would be

\[ L = \langle \Psi \rvert H \lvert \Psi \rangle \qquad H = 1 - \frac{1}{2}\left(Z(0) + Z(1)\right) \]

The Hamiltonian could also be written as

\[ H = 2\lvert 11 \rangle \langle 11 \rvert + \lvert 10 \rangle \langle 10 \rvert + \lvert 01 \rangle \langle 01 \rvert \]

Tequila provides the convenience function tq.gates.Projector to initialize Hamiltonians like that

2*tq.paulis.Projector("|11>") + tq.paulis.Projector("|01>") + tq.paulis.Projector("|10>")

The projector can also be initialized with more structured QubitWaveFunctions which can itself be initialized from array or string.
Here are some examples

wfn = tq.QubitWaveFunction.from_string("1.0*|00> + 1.0*|11>")
wfnx = tq.QubitWaveFunction.from_array(arr=[1.0, 0.0, 0.0, 1.0])
print(wfn == wfnx)
wfn = wfn.normalize()
print(wfn)

P = tq.paulis.Projector(wfn=wfn)
print("P = ", P)

Apart from Projector there is also KetBra which intialized more general operators like \[ \lvert \Psi \rangle \langle \Phi \rvert \]

Keep in mind that those are not hermitian.
But they can be split up into their hermitian and anti hermitian part where both can then be used as hamiltonians for expectationvalues.

If the hermitian = True key is set, the function returns the hermitian version of the operator (which is the same as the hermitian part of the old operator)

\[ \frac{1}{2}\left(\lvert \Psi \rangle \langle \Phi \rvert + \lvert \Phi \rangle \langle \Psi \rvert \right) \]

wfn1 = tq.QubitWaveFunction.from_string("1.0*|00> + 1.0*|11>").normalize()

op = tq.paulis.KetBra(bra=wfn1, ket="|00>")

H1, H2 = op.split()

print("operator=", op)
print("hermitian part      = ", H1)
print("anti-hermitian part =", H2)

H = tq.paulis.KetBra(bra=wfn1, ket="|00>", hermitian=True)
print("hermitianized operator = ", H)

Can I do basic operations on wavefunctions and operators without quantum backends?

In principle yes. But keep in mind that tequila was not made for this.
However, some of those operations might come in handy for debugging or small examples.

You can not execute circuits without a simulator since they are just abstract data types (no matrices or anything). Tequila has however its own small debug simulator backend = symbolic but there is no reason to use it if you have any other quantum backend installed.

Hamiltonians can be converted to matrices.

We give a few examples here

wfn = tq.QubitWaveFunction.from_string("1.0*|0> + 1.0*|1>").normalize()
H = 1.0/numpy.sqrt(2.0)*(tq.paulis.Z(0) + tq.paulis.X(0))
wfn2 = wfn.apply_qubitoperator(H).simplify()

print("|wfn>  = ", wfn)
print("H      = ", H)
print("H|wfn> = ", wfn2)
wfn1 = tq.QubitWaveFunction.from_string("1.0*|0> + 1.0*|1>").normalize()
wfn2 = tq.QubitWaveFunction.from_string("1.0*|0> - 1.0*|1>").normalize()
print("<wfn1|wfn2> = ", wfn1.inner(wfn2))
H = 1.0/numpy.sqrt(2.0)*(tq.paulis.Z(0) + tq.paulis.X(0))
print(H.to_matrix())

Can I import an Hamiltonian from OpenFermion?

Yes! OpernFermion is currently tequilas backend for Hamiltonians, which makes importing from it straight forward. You just need to wrap the OpenFermion QubitOperator into tequilas QubitHamiltonian.

We show a few examples

from openfermion import QubitOperator

# get OpenFermion QubitOperator from tequila QubitHamiltonian
H = tq.paulis.X(0)
of_operator = H.to_openfermion()

print("{} = {}".format(type(H), H))
print("{} = {}".format(type(of_operator), of_operator))

# init tequila QubitHamiltonian with OpenFermion QubitOperator
H = tq.QubitHamiltonian.from_openfermion(of_operator)
print("{} = {}".format(type(H), H))

# initialization from file os often read in the string form
of_string = str(of_operator)
tq_string = str(H)

print(of_string)
print(tq_string)

H = tq.QubitHamiltonian.from_string(string=of_string, openfermion_format=True)
print(H)
H = tq.QubitHamiltonian.from_string(string=tq_string, openfermion_format=False)
print(H)

Can I compile into a regular function instead of one which takes dictionaries?

Not recommended but yes. The order of the function arguments is the order you get from extract_variables

U = tq.gates.Ry(angle="a", target=0)
U += tq.gates.X(power = "b", target=1)
H = tq.QubitHamiltonian.from_string("X(0)Z(1)")
E = tq.ExpectationValue(H=H, U=U)

f = tq.compile_to_function(E)

print("order is : ", E.extract_variables())
print(f(0.5, 1.0))
print(tq.simulate(E, variables={"a":0.5, "b":1.0}))

If you also want to fix the samples and other entries to your compiled objective you can build wrappers

def mywrapper(compiled_obj, samples):
    return lambda *x: compiled_obj(*x, samples=samples)

wrapped = mywrapper(f, samples=100)

# don't expect same results, since samples are taken individually
print(wrapped(1.0, 0.5)) # always takes 100 samples
print(f(1.0, 0.5, samples=100)) # samples need to be given
print(f(1.0, 0.5, samples=1000)) # but sampling rate can be changed
print(f(1.0, 0.5)) # you can go back to full simulation which you cannot with the wrapped function

How do numerical gradients work?

Yes this is possible by passing for example gradient={'method':'2-point', 'stepsize': 1.e-4} to the tq.minimize function.

The default is a central 2-point derivative stencil where h is the stepsize: \[\displaystyle \frac{\partial f}{\partial a} = \frac{f(a+\frac{h}{2}) - f(a-\frac{h}{2})} {h} \]

Other methods are: 2-point-forward: Forward derivative stencil:

\[\displaystyle \frac{\partial f}{\partial a} = \frac{f(a+h) - f(a)} {h} \]

2-point-backward: Backward derivative stencil:

\[\displaystyle \frac{\partial f}{\partial a} = \frac{f(a) - f(a-h)} {h} \]

You can also use your own numerical derivative stencil by passing a callable function as method.
The function should have the signature which is given in the example below.

Here is an example:

import tequila as tq
# form a simple example objective
H = tq.paulis.X(0)
U = tq.gates.Ry(angle="a", target=0)
E = tq.ExpectationValue(U=U, H=H)

# make it more interesting by using analytical gradients for the objective
# and numerical gradients to optimize it

objective = tq.grad(E, 'a')**2 # integer multiples of pi/2 are minima

# start from the same point in all examples
initial_values = {'a': 2.3}
# optimize with analytical derivatives
result = tq.minimize(objective=objective, method="bfgs", initial_values=initial_values)
#result.history.plot("energies")
#result.history.plot("gradients")

# optimize with 2-point stencil
result = tq.minimize(objective=E, method="bfgs", gradient={'method': '2-point', 'stepsize':1.e-4}, initial_values=initial_values)
#result.history.plot("energies")
#result.history.plot("gradients")

# optimize with custom stencil
# here this is the same as the default
import copy
def mymethod(obj, angles, key, step, *args, **kwargs):
    left = copy.deepcopy(angles)
    left[key] += step / 2
    right = copy.deepcopy(angles)
    right[key] -= step / 2
    return 1.0 / step * (obj(left, *args, **kwargs) - obj(right, *args, **kwargs))

result = tq.minimize(objective=E, method="bfgs", gradient={'method': mymethod, 'stepsize':1.e-4}, initial_values=initial_values)
#result.history.plot("energies")
#result.history.plot("gradients")

# optimize with a scipy method and use scipys 2-point
# the scipy protocol will have more function evaluations and less gradient evaluations for some methods
# the stepsize in scipy has to be passed with the `method_options` dictionary
# with the keyword `eps`
result = tq.minimize(objective=E, method="bfgs", gradient='2-point', method_options={'eps':1.e-4}, initial_values=initial_values)
#result.history.plot("energies")

Can I use the numerical gradient protocols from SciPy?

Yes you can for all scipy methods by passing gradient='2-point' to tq.minimize.
See the scipy documentation for the stepsize and more options which can be passed with method_options dictionary where the key for the stepsize is usually eps. Note that not all scipy methods support numerical gradients, but you can always fall back to tequilas numerical gradients. See the previous cell for an example.