Skip to content
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

Merged
merged 16 commits into from
Aug 21, 2024

Conversation

mho291
Copy link
Contributor

@mho291 mho291 commented May 17, 2024

Checklist:

  • Reviewers confirm new code works as expected.
  • Tests are passing.
  • Coverage does not decrease.
  • Documentation is updated.

Copy link

codecov bot commented May 17, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 99.94%. Comparing base (37e1010) to head (4455a70).
Report is 66 commits behind head on master.

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           
Flag Coverage Δ
unittests 99.94% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@mho291 mho291 marked this pull request as ready for review May 30, 2024 05:57
@MatteoRobbiati MatteoRobbiati marked this pull request as draft June 11, 2024 09:23
@MatteoRobbiati MatteoRobbiati marked this pull request as ready for review June 11, 2024 09:23
Copy link
Contributor

@MatteoRobbiati MatteoRobbiati left a 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.

Comment on lines 73 to 74
circuit_no_meas = circuit.__class__(**circuit.init_kwargs)
circuit_meas = circuit.__class__(**circuit.init_kwargs)
Copy link
Contributor

@MatteoRobbiati MatteoRobbiati Jul 16, 2024

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.

Copy link
Contributor Author

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!

Copy link
Contributor

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)

Copy link
Contributor Author

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)

else:
circuit_meas.add(gate)

noisy_circuit = circuit.__class__(**circuit.init_kwargs)
Copy link
Contributor

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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you applying $U$ and $U^{\dagger}$ only when $U$ is found?
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.

Copy link
Contributor Author

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 CNOTs and RX(pi/2) gates are being folded, because this doesn't seem very viable to me, especially when there are no CNOTs 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.

Copy link
Contributor

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?

Copy link
Contributor

@AlejandroSopena AlejandroSopena Aug 7, 2024

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).

@scarrazza scarrazza added this to the Qibo 0.2.11 milestone Jul 24, 2024
Copy link
Contributor

@AlejandroSopena AlejandroSopena left a 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.

src/qibo/models/error_mitigation.py Outdated Show resolved Hide resolved
src/qibo/models/error_mitigation.py Outdated Show resolved Hide resolved
src/qibo/models/error_mitigation.py Outdated Show resolved Hide resolved
src/qibo/models/error_mitigation.py Outdated Show resolved Hide resolved
src/qibo/models/error_mitigation.py Outdated Show resolved Hide resolved
src/qibo/models/error_mitigation.py Outdated Show resolved Hide resolved
src/qibo/models/error_mitigation.py Show resolved Hide resolved
src/qibo/models/error_mitigation.py Outdated Show resolved Hide resolved
@AlejandroSopena
Copy link
Contributor

global_unitary_folding is missing in the docstrings of ZNE().

@mho291
Copy link
Contributor Author

mho291 commented Aug 8, 2024

global_unitary_folding is missing in the docstrings of ZNE().

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.

3reg_graph_ZNE_approxratio

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.

@mho291
Copy link
Contributor Author

mho291 commented Aug 20, 2024

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!

@AlejandroSopena
Copy link
Contributor

global_unitary_folding is missing in the docstrings of ZNE().

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.

3reg_graph_ZNE_approxratio

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.

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.

Copy link
Contributor

@AlejandroSopena AlejandroSopena left a 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

@scarrazza scarrazza self-requested a review August 21, 2024 09:05
@scarrazza scarrazza merged commit 386fe25 into master Aug 21, 2024
27 checks passed
@alecandido alecandido deleted the ZNE_get_noisy_circuit branch August 28, 2024 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants