qiboteam / qibo

A framework for quantum computing
https://qibo.science
Apache License 2.0
288 stars 59 forks source link

General unitary gates #231

Closed AdrianPerezSalinas closed 2 years ago

AdrianPerezSalinas commented 4 years ago

Sometimes the algorithms, in particular the variational ones, require the implementation of general unitary gates. Unitary gates must always be decomposed in terms of basis gates to be implemented in the quantum computer, but for simulation it might be more efficient if the general gates are directly applied

Would it be possible to add some function that allows the user to create user-defined gates for calling them in a particular program?

stavros11 commented 4 years ago

We have gates.Unitary that allows you to define a gate that acts on an arbitrary number of qubits via its matrix. For example gates.Unitary(np.random.random((4, 4)), 0, 1) will define a random 4x4 matrix that acts on qubits 0 and 1. Note that there is no check that the matrix you pass is actually unitary, the user just has to be consistent with this. Does this cover the functionality you mean?

Also the "custom" backend will work for up to two-qubit unitaries. If you want to define a three or more-qubit unitary you have to switch to Tensorflow backends (these are slower).

AdrianPerezSalinas commented 4 years ago

I think so I assume that this function does not accept parameters and any parametrization must be done outside the function, isn't it? And for 2 x 2 matrix it is as simple as gates.Unitary(np.random.random((2,2)), 0)

Are there any further issues to be aware of with respect to the backends?

stavros11 commented 4 years ago

I assume that this function does not accept parameters and any parametrization must be done outside the function, isn't it? And for 2 x 2 matrix it is as simple as gates.Unitary(np.random.random((2,2)), 0)

Yes, the 2x2 matrix works like this. The matrix that you pass can be variational and you can also update it normally using circuit.set_parameters (if it is inside a circuit). If you need to do a calculation using some variational parameters to obtain the final matrix, indeed this has to be done outside the function and then gate should be created using the final matrix.

Are there any further issues to be aware of with respect to the backends?

Up to two-qubit gates (that is either 2x2 or 4x4 matrices) should work with any backend. Three-qubit gates or more (8x8 or higher) should work only with "defaulteinsum" and "matmuleinsum" (not "custom").

AdrianPerezSalinas commented 4 years ago

Okay, backends are understood.

Even though I might be repetitive, would it be easy (for the average user) to create a gate with the same functionality and usage as, for instance, RX, but being an arbitrary unitary gate?

I am trying to write a circuit with the gate Rz() Ry() Rz() and would like to fuse them all in a single U3 gate

AdrianPerezSalinas commented 4 years ago

Thank you very much for your replies, they are being very useful!!

stavros11 commented 4 years ago

I am trying to write a circuit with the gate Rz() Ry() Rz() and would like to fuse them all in a single U3 gate

There are two ways to implement this with what we have currently in Qibo, but each may have some complications:

  1. Use circuit.fuse(), for example

    c.add([RZ(0, theta=1), RY(0, theta=1), RZ(0, theta=1)])
    fused_c = c.fuse()
    print(c.queue) # has len = 3
    print(fused_c.queue) # has len = 1

    This creates a new circuit in which gates are fused up to two-qubits (see docs for more details). For example in the above case the three rotations will be fused to a single Unitary gate. Two disadvantages of this approach are that the fusion algorithm has some overhead and may not be efficient for small circuits (with less than 10 qubits) and sometimes (for more complicated circuits) the gates may not be fused as you expect (although one qubit gates acting in order on the same qubit should always be fused into one Unitary). The advantage of using fuse is that it works well with set_parameters, if you want to update the variational parameters. In the above example

    c.set_parameters([1, 2, 3])
    # is equivalent to 
    fused_c.set_parameters([1, 2, 3])
  2. Fuse gates yourself before adding them to the circuit. Qibo gates support matrix multiplication using @, so the above example would work as:

    g1 = gates.RZ(0, theta=1)
    g2 = gates.RY(0, theta=1)
    g3 = gates.RZ(0, theta=1)
    c.add(g3 @ g2 @ g1)

    This will add a single Unitary gate in the circuit that is the product of the three. Note that the order of the product is reversed because g1 is the first gate to act on the statevector and @ works as gate matrix multiplication. The advantage of this is that you have better control on which gates are fused (while c.fuse() is totally automatic), however if you want to update parameters you have to create the new unitary matrix. So c.set_parameters([1, 2, 3]) will not work, because c doesn't know that you fused three gates to create it. Instead you need to calculate the new unitary as a 2x2 array:

    g1 = gates.RZ(0, theta=1)
    g2 = gates.RY(0, theta=2)
    g3 = gates.RZ(0, theta=3)
    g = g3 @ g2 @ g1
    c.set_parameters([g.matrix])

Regarding the RZ-RY-RZ, I noticed that this combination is used in various examples. If you think this is very common, we can add a U3 gate in Qibo. In this case, could you please write the matrix convention that you would prefer for this? Is it a product of the RZ and RY gates we already have with three different parameters? We will have to add this before Monday if we want it included in v0.1.0.

AdrianPerezSalinas commented 4 years ago

My idea was to update parameters, so I think the first version suits better my needs. However, at this point I need to apply it to very small circuits, so I think it might not be the optimal approach. I have to face the problem and figure the best solution out.

With respect to adding the operation to the base code of the package, I believe this might be a long-term solution, and we may want to include it together with some other special gates (U1, U2, U3). We can discuss it in the future. For the record, I think the convention should be

U3(theta, lambda, phi) = [[cos(theta/2)e^(-i \lambda / 2)e^(-i \phi / 2), -sin(theta/2)e^(i \lambda / 2)e^(-i \phi / 2)], [sin(theta/2)e^(-i \lambda / 2)e^(i \phi / 2), cos(theta/2)e^(i \lambda / 2)e^(i \phi / 2)]]

U2 (lambda, phi) = U3(pi / 2, lambda, phi) = 1 / sqrt(2) * [[e^(-i \lambda / 2)e^(-i \phi / 2), -e^(i \lambda / 2)e^(-i \phi / 2)], [e^(-i \lambda / 2)e^(i \phi / 2), e^(i \lambda / 2)e^(i \phi / 2)]]

U1 (lambda) = ZPow(lambda)

stavros11 commented 4 years ago

With respect to adding the operation to the base code of the package, I believe this might be a long-term solution, and we may want to include it together with some other special gates (U1, U2, U3). We can discuss it in the future.

I agree with adding these special gates. I will open a PR implementing this using the convention you provided. This won't be included in the pip version until we make a new release, however once it is ready and merged it would be possible to use it if you clone the repository and build from source.

My idea was to update parameters, so I think the first version suits better my needs. However, at this point I need to apply it to very small circuits, so I think it might not be the optimal approach. I have to face the problem and figure the best solution out.

Until the U1, U2, U3 gates are added, a method to do this without using circuit.fuse() is the following: Define a function that calculates your unitary as a numpy array:

def u3matrix(theta, lambda, phi):
    return ... # the matrix you wrote in your comment as np.array

and use it as follows

# create circuit with a U3 gate acting on qubit 0
# use initial value 0 for all U3 parameters
c = Circuit(2)
c.add(gates.Unitary(u3matrix(0, 0, 0), 0))
# update the values of parameters to 1, 2, 3
c.set_parameters([u3matrix(1, 2, 3)])

This would be a bit more complicated if you have more parametrized gates, eg:

c = Circuit(2)
c.add(gates.RX(0, theta=0))
c.add(gates.RX(1, theta=0))
c.add(gates.Unitary(u3matrix(0, 0, 0), 0))
# set theta=1 in RX rotations and (1, 2, 3) in U3
c.set_parameters([1, 1, u3matrix(1, 2, 3)])

but I think if you play a little bit with creating the set_parameters list for your circuit this method would work without any fusion overhead.

AdrianPerezSalinas commented 4 years ago

Oh, nice! Thank you very much, I think this could fulfills my needs! I will give it a try and see whether it works or not. It is very helpful that you have so much knowledge about the inner working of Qibo!