-
Notifications
You must be signed in to change notification settings - Fork 60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added global unitary folding for get_noisy_circuits #1327
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #1327 +/- ##
=======================================
Coverage 99.94% 99.94%
=======================================
Files 78 78
Lines 11236 11248 +12
=======================================
+ Hits 11230 11242 +12
Misses 6 6
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this!
Some comments follows, together with a more general comment here:
I think the copy of the circuit can be done faster: you could create a circuit of the same size of the target similarly to what you are doing, adding only non-M gates and, later, you can append the circuit.measurements
gates, which is the part of circuit.queue
containing the measurements only.
src/qibo/models/error_mitigation.py
Outdated
circuit_no_meas = circuit.__class__(**circuit.init_kwargs) | ||
circuit_meas = circuit.__class__(**circuit.init_kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these copies of the circuit? If yes, I would use the circuit.copy
method.
I understand you want to initialize the circuit in the same way the original circuit was initialized. I would probably prefer using Circuit(**circuit.init_kwargs)
instead of how you are doing here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Matteo, the purpose of lines L73 to L79, which reads,
circuit_no_meas = circuit.__class__(**circuit.init_kwargs)
circuit_meas = circuit.__class__(**circuit.init_kwargs)
for gate in circuit.queue:
if gate.name != "measure":
circuit_no_meas.add(gate)
else:
circuit_meas.add(gate)
is to duplicate the input circuit
. As its name suggests, circuit_no_meas
is the input circuit
without the measurements. Likewise, circuit_meas
is the input circuit
with measurements.
The reason I opted to have lines L73 to L79 is so that we can then construct our noisy_circuit
by first creating a copy of circuit_no_meas
.
Then we perform global unitary folding in lines L83 to L86, which reads,
for _ in range(num_insertions):
noisy_circuit += circuit_no_meas.invert() + circuit_no_meas
noisy_circuit += circuit_meas
Would you advise to use circuit.copy
or Circuit(**circuit.init_kwargs)
to copy the circuit? What does the latter do?
Thanks very much!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I mean is that you don't need two copies of the circuit to do what you aim to do:
from qibo import Circuit, gates
# build your circuit
c = Circuit(4)
for q in range(4):
c.add(gates.H(q))
c.add(gates.CNOT(0,1))
c.add(gates.CNOT(3,1))
c.add(gates.M(0,3))
# initialize a copy with same arguments (dense, accelerators, etc)
copy_c = Circuit(**c.init_kwargs)
# populate the copy with non-M gates
for g in c.queue:
if not isinstance(g, gates.M):
copy_c.add(g)
# start building noisy circuit with U^+ and U
noisy_c = copy_c.invert() + copy_c
# add measurements according to original circuit
for m in c.measurements:
noisy_c.add(m)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right! Thanks! I will use your implementation in the code. But we will need to add one more line before we build the noisy circuit so that our final circuit is U (U^+ U)^n
for n
repetitions of (U^+ U)
.
# initialize `noisy_c` as `copy_c`.
noisy_c = copy_c
# start building noisy circuit with U^+ and U
noisy_c = copy_c.invert() + copy_c
# add measurements according to original circuit
for m in c.measurements:
noisy_c.add(m)
src/qibo/models/error_mitigation.py
Outdated
else: | ||
circuit_meas.add(gate) | ||
|
||
noisy_circuit = circuit.__class__(**circuit.init_kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See previous comment.
for gate in circuit.queue: | ||
noisy_circuit.add(gate) | ||
|
||
if isinstance(gate, i_gate): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you applying
Is this a common practice? Otherwise, we could be more general and ask the user to select the position in the queue (or in the graph, speaking more qibo-core
friendly) where to apply the gate-inverse mechanism.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that this is the part of the code mostly original. I cannot remember if I tweaked anything here.
I was also curious as to why only CNOT
s and RX(pi/2)
gates are being folded, because this doesn't seem very viable to me, especially when there are no CNOT
s nor RX(pi/2)
gates in the input circuit
. Hence I added the global unitary folding which makes more sense since we're folding the circuit to artificially multiply the noise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe @AlejandroSopena do you have a clearer picture of the ZNE implementation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MatteoRobbiati , what you propose can be implemented. In fact, it is common to fold some gates at random. However, if the fidelity of each gate is different, then the expected value in the zero-noise limit cannot be calculated as a linear superposition of expected values at different noise levels as we do. We would need to implement a numerical fit (see the comment in my review).
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @mho291.
I agree that the implementation of ZNE is not as general as it could be and that it can be extended by implementing global unitary folding. We have used local unitary folding because this way we have finer control over the circuit noise, meaning the noise increases less with each noise level compared to global unitary folding.
This allows using Richardson extrapolation to obtain an expected value in the zero-noise limit. In other words, we construct a function where the expected value in the zero-noise limit is a linear combination (with analytically known coefficients) of the expected values at different noise levels (line 215). This is equivalent to expressing the expected value of an observable as a function of the noise as an n-degree polynomial where n + 1 is the number of noise levels. The "problem" with this approach is that we need n + 1 noise levels to perform an n-degree polynomial fit (see arxiv:1612.02058). When the data points that are experimentally accessible all reflect a fairly high amount of noise, it is not possible to reach a sufficient number of noise levels. In global unitary folding, this is usually the case as the noise increases faster. Then, one must fit the data, typically to a lower-degree polynomial or an exponential decay (see arxiv:2005.10921)
I agree with implementing global unitary folding, but with the current method of calculating the expected value in the zero-noise limit, it might not be useful in practice.
|
Thanks @AlejandroSopena for reviewing the code. I will amend according to your suggestions but will take some time as I am now away. Nonetheless I'd like to share some results using global unitary folding on IQM! We used ZNE to extrapolate and estimate the ideal value of the approximation ratio, which is the cut_size/max_cut using QAOA for a fairly large graph of 10 nodes (hence 10 qubits). The QAOA only had one layer and the circuit was at a depth of 21. We used global unitary folding. I was pretty impressed at the ZNE's use of Richardson extrapolation! It does seem like we cannot push the ZNE any further beyond maybe 6 or 7 global unitary foldings because the plateau is imminent, so you're right that we might need to use a different fit for the data if the circuit depth is too large. |
Hi @MatteoRobbiati and @AlejandroSopena. Sorry for the late reply, I just returned to work after mandatory national service. Just wondering if we can expedite the merging of this PR. We're in the midst of writing up an AWS blogpost for our Qibo-AWS work where we have results based on global unitary folding. Unfortunately our team cannot release the blogpost to the public unless this PR merged and we have a deadline to meet. Thanks so much! |
These results are very good! This is a good example where global unitary folding works with Richardson extrapolation because the initial circuit is shallow, so there is still room to increase the noise. Even so, an exponential decay would also work with fewer noise levels. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the changes @mho291
Checklist: