Closed igres26 closed 3 years ago
Thanks for this summary. When I read this approach, which let me call it "conditional circuit", I think that we should reconsider the circuit queue as a DAG instead of a simple list of operations. The DAG can contain conditionals based on output of other nodes. I see 2 possible approaches, emulate a dag sequentially in the code, e.g. keeping the queue of gates and updating the circuit execute method, or replace the queue with a dag object (custom made or from other libraries) which will conditionally apply gates to the state.
I wanted to add some information here. I think that the best possible way to do this is to emulate what quantum computers do. That is, when we measure one qubit, the rest of the wavefunction collapses in some way, what can be as Stefano said a conditional action. However, as it is now, I see two problems:
The topic is very interesting and I would like to contribute. From my point of view we should split this issue in two problems. The first one is obvious, we should develope a simulator that simulate exactly what a quantum computer can do (and probably develope a version to simulate the hardware devices we are creating at TII).
The second one (as Sergi, I think, was suggesting) it would be to create some debugging tools to help the programmer writing (complex) quantum algotirhms. I think this is something missing in most of quantum frameworks and I do not know if a debugger is part of the Qibo roadmap.
@AdrianPerezSalinas @igres26 let me try to understand better the primitives needed to make this thing work. The current Qibo layout accepts a "eager" model computation model, e.g. we could already retrieve results step by step for all gates:
# allocate initial state
state = np.zeros(2**n, dtype=np.complex128)
state[0] = 1 + 0j
# apply gates
from qibo import gates
gates.H(0)(state)
if state <condition>:
gates.H(1)(state)
Is this approach sufficient to provide the needed flexibility?
I guess so. Can this <condition>
encode something like the 0-th qubit is in the state |0> ? This could happen in this case with probability 50%
I don't think we should be conditioning on the state. The condition should be on a measurement result as one would on a real device. This should probably begin with a rework on how measurements work when they appear inside the circuit.
An example of the ideal usage that is already present in qibo is the one found in the quantum_order_finding_semiclassical(N, a)
function in the shor example functions.py
file.
The ideal functionality would be not creating and running a new circuit for each measurement, but still controlling on the previous measurement results.
I don't think we should be conditioning on the state. The condition should be on a measurement result as one would on a real device. This should probably begin with a rework on how measurements work when they appear inside the circuit.
yes, I think it is possible do adjust my example by replacing H with the M gate.
I think in terms of implementation this can be simplified to two steps:
but there are at least the following challenges associated even with just (1):
gates.M
which is not treated as a gate internally - it is not added to the queue).Also some comments on Adrian's remarks regarding what is currently implemented in Qibo:
- There is no possible measurement to be done during the execution of the circuit. We can do a measurement gate, and then everything stops, or we can do a collapse gate that makes everything change with respect to what we have told it to do. Combination of both features does not exist.
This is correct, it is not possible to do measurements during execution because of the following limitations:
The collapse gate performs the collapse according to the result you specify, however this has the nan
issue Sergi explained in the first post. With that in mind I am not sure if keeping the collapse as it is now is a good idea.
In principle combination of these features is possible if you use different circuits:
c1 = Circuit(...)
c1.add(...)
c1.add(gates.M(0))
result = c1(nshots=1)
result_value = result.samples()[0, 0] # get shot result (0 or 1)
c2 = Circuit(...)
c2.add(gates.Collapse(0, result=result_value)
c2.add(...)
result = c2(initial_state=c1.final_state)
which is the method used in the semi-classical Shor example.
- Let us have a composite system (0)(rho), where () stands for quantum states. Then we apply a Hadamard gate in the first qubit,
A physics question independently of Qibo: Is it correct that the Hadamard acts this way? If you start from the composite |0><0| (rho) and you apply H to the first qubit you get: H |0><0| H (rho) = (|0> + |1>)(<0| + <1|) (rho) / 2 which is different than |0><0| (rho) + |1><1| (rho) because it also has non-diagonal terms |0><1| and |1><0|.
and a conditional X gate (a CNOT somehow). We would have (0)(rho) + (1)(X rho X).
This is possible to do in Qibo using the CNOT, assuming you properly create the (|0><0| + |1><1|) (rho) state:
# Generate a random 2-qubit density matrix
rho = np.random.random((4, 4))
idx = np.arange(4)
rho[idx, idx] = rho[idx, idx] / np.trace(rho) # normalization so that Tr(rho) = 1
# note that this rho may not be positive but it doesn't matter for the sake of this example
# initial state (|0><0| + |1><1|) rho / 2 = I x rho / 2
initial_rho = np.kron(np.eye(2) / 2, rho)
c = Circuit(3, density_matrix=True)
c.add(gates.CNOT(0, 1))
final_rho = c(initial_rho).state()
Here I created the proper initial state as an array, because just adding an gates.H(0)
before the CNOT will not work because of what I wrote above (will create non-diagonal terms). You can check that in this example final_rho
is indeed the density matrix of (0)(rho) + (1)(X rho X). For example you can create this using just numpy:
from qibo import matrices
zero = np.array([[1, 0], [0, 0]]) # zero state |0><0|
one = np.array([[0, 0], [0, 1]]) # one state |1><1|
xgate = np.kron(matrices.X, matrices.I) # X gate acting on the first of two qubits
# calculate the target final rho: |0><0| rho + |1><1| X rho X
target_rho = (np.kron(zero, rho) + np.kron(one, xgate.dot(rho.dot(xgate)))) / 2
and check that target_rho
is the same as final_rho
.
Now we measure and obtain either (0)(rho) or (1)(X rho X) with different probabilities. Is there any way to keep track of everything as a single density matrix? I do not see the manner.
Currently you can only do one of the following:
gates.M(0)
in the above circuit, however all this will do is just give you 0 or 1 with equal probability (because Tr(rho) = Tr(X rho X) for any rho) with no collapse or anything. gates.Collapse(0, result=0)
to collapse to (0)(rho) or gates.Collapse(0, result=1)
to collapse to (1) X(rho)X.A possible API for the controlled by measurement gates could be the following:
c = Circuit(2)
c.add(...)
m0 = gates.M(0) # measurement on qubit-0
c.add(m0)
c.add(gates.X(0).controlled_by(m0))
c.add(gates.X(1).controlled_by(m0))
This would apply the X gates only if qubit-0 is found 1 when measured. This should also be easy to implement as the measurement result can be cached within the m0
object and used in the controlled gates.
Following is an example on what the semiclassical function in Shor would look like with a possible new API.
def quantum_order_finding_semiclassical_new_API(N, a):
"""Quantum circuit that performs the order finding algorithm using a semiclassical iQFT.
Args:
N (int): number to factorize.
a (int): chosen number to use in the algorithm.
Returns:
s (float): value of the state measured by the quantum computer.
"""
print(' - Performing algorithm using a semiclassical iQFT.\n')
# Creating the parts of the needed quantum circuit.
n = int(np.ceil(np.log2(N)))
b = [i for i in range(n+1)]
x = [n+1+i for i in range(n)]
ancilla = 2*n + 1
q_reg = 2*n + 2
circuit = Circuit(2*n+3)
print(f' - Total number of qubits used: {2*n+3}.\n')
r = []
exponents = []
exp = a%N
for i in range(2*n):
exponents.append(exp)
exp = (exp**2)%N
# Building the quantum circuit
circuit.add(gates.X(x[len(x)-1]))
# Using multiple measurements for the semiclassical QFT.
for i in range(0, 2*n):
if r[-1] == 1:
circuit.add(gates.X(q_reg))
circuit.add(gates.H(q_reg))
circuit.add(c_U(q_reg, x, b, exponents[-1-i], N, ancilla, n))
angle = 0
for k in range(2, i+2):
angle += 2*np.pi*r[i+1-k]/(2**k)
circuit.add(gates.U1(q_reg, -angle))
circuit.add(gates.H(q_reg))
output = circuit.add(gates.M(q_reg, collapse=True))
r.append(output)
s = 0
for i in range(2*n):
s += r[i]*2**(i)
print(f"The quantum circuit measures s = {s}.\n")
return s
Do you think that is intuitive? We can use this as a reference to build up from.
Just to be sure, the only new thing is
output = circuit.add(gates.M(q_reg, collapse=True))
isn't it?
In my opinion this is the class of syntax we should look for. Something quick, intuitive, and as a matter of fact it is what it is, a measurement gate. My question is: now we keep the result (0, 1) and the rest of the quantum state behaves to keep that result, so there is only some loss of coefficients and terms. I think this can be done both for density matrices and statevectors, do not know what you think, @stavros11
That line, and the fact that no new circuit need to be created, and there is no need to save the final state after each circuit execution.
This is coded so that each pass only provides a single sample. And if statistics are required the circuit has to be executed again from the top.
As we aim to tackle more complex quantum algorithms we will need to upgrade our system of performing measurements during the execution of a circuit.
Until now, measurements do not alter the wave function of the quantum state, and in order to fix that, the Collapse gate was introduced. This can be used to simulate measurement but requires to start a new Circuit object every time a measurement is taken (see Shor example).
Collapse also appears to have issues when trying to collapse a state into a non-existent output, try:
where
nan
values appear in the wave function.Ideally one would want to add measurements during a circuit, and then add gates or make alterations to the circuit conditioned to the result of the measurement result. This approach would make sense in a setting where every circuit execution yields one result, as keeping track of all options at the same time would require exponential memory.
This would not only be useful for the semi-classical Shor example, but on future simulations of quantum error correcting codes.