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

Use average gate fidelity in the commutation checker #13874

Merged
merged 22 commits into from
Mar 6, 2025

Conversation

Cryoris
Copy link
Contributor

@Cryoris Cryoris commented Feb 18, 2025

Summary

The commutation checker currently uses a different metric to check when gates commute than other places in Qiskit, such as the RemoveIdentityEquivalent. This leads to strange behavior, where one gate is treated as identity in the commutation, but is subsequently not removed as other passes do not treat it as identity. This is what causes #13547.

This PR updates the commutation checker to use the same average gate fidelity and thus the transpiler to handle almost-identity gates more consistently.

Closes #13547.

Details and comments

  • Still needs a reno.
  • This has one small caveat: the definition of the average gate fidelity is global phase agnostic, but our commutation rules are not. I.e. we currently do not want to commute two gates, if doing that introduces a global phase. We could extend this in the future, but this is outside the scope of this PR.
  • This PR increases the threshold for gates to commute (== rotation angles must now be smaller than before in order to commute). This means the #gates we transpile a QFT to increases, see the benchmark below. However, @ElePT ran benchpress and this didn't seem to cause a noticable regression overall.

Here's a small benchmark comparing transpile time, CX depth and count for the (1) the QFT, (2) an efficient SU2 circuit with log(num qubits) depth and (3) a Heisenberg Trotter evolution on a line.

image

@Cryoris Cryoris requested a review from a team as a code owner February 18, 2025 17:36
@qiskit-bot
Copy link
Collaborator

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core

@Cryoris Cryoris added this to the 2.0.0 milestone Feb 18, 2025
@Cryoris
Copy link
Contributor Author

Cryoris commented Feb 24, 2025

This currently fails because the error tolerance for the matrix-based checks is too low. This leads e.g. to the matrix-based check of [H, H] to return False. For this to be more robust we'd have to increase the check threshold, the question is whether to just use some const * eps or whether to base it on the matrix dimension.

these cover cases where we accumulate roundoff errors
- increased the threshold for value comparisons to 16 EPS
- use same functions in RemoveIdentityEquiv and CC checker
- add reno
- add detailed docs
- add some tests for above thresholds
@coveralls
Copy link

coveralls commented Mar 3, 2025

Pull Request Test Coverage Report for Build 13687167568

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 142 of 148 (95.95%) changed or added relevant lines in 7 files are covered.
  • 260 unchanged lines in 24 files lost coverage.
  • Overall coverage decreased (-0.2%) to 87.002%

Changes Missing Coverage Covered Lines Changed/Added Lines %
crates/accelerate/src/gate_metrics.rs 33 34 97.06%
crates/accelerate/src/commutation_checker.rs 64 66 96.97%
crates/accelerate/src/remove_identity_equiv.rs 16 19 84.21%
Files with Coverage Reduction New Missed Lines %
crates/accelerate/src/unitary_compose.rs 1 76.09%
crates/accelerate/src/unitary_synthesis.rs 1 94.39%
crates/qasm2/src/expr.rs 1 94.23%
qiskit/primitives/utils.py 1 92.86%
qiskit/result/distributions/quasi.py 1 95.92%
qiskit/result/sampled_expval.py 2 93.55%
qiskit/transpiler/passes/optimization/template_matching/template_substitution.py 2 93.29%
qiskit/utils/units.py 3 90.91%
qiskit/primitives/base/base_sampler.py 5 79.17%
qiskit/circuit/parameterexpression.py 6 97.68%
Totals Coverage Status
Change from base Build 13673594587: -0.2%
Covered Lines: 76053
Relevant Lines: 87415

💛 - Coveralls

which is consistent with the 2qWeylDecomp and still stricter than Qiskit 1.x
Copy link
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

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

Overall this LGTM, it'll be good to have everything consistent. Just a few inline comments.

// If the average gate infidelity is below this tolerance, they commute. The tolerance
// is set to max(1e-12, 1 - approximation_degree), to account for roundoffs and for
// consistency with other places in Qiskit.
let tol = 1e-12_f64.max(1. - approximation_degree);
Copy link
Member

Choose a reason for hiding this comment

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

This does mean we can't approximate more than what a 1e-12 tolerance provides. But it's the same logic we use in other places so I think that's fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah it is unfortunately. I don't like this very much either, but currently approximation_degree is 1. everywhere per default which doesn't really allow us to set another default like 1-1e-12. The other option would've been to use None as indicator, but that already means that the target error rates should be used.

Comment on lines 650 to 662
let dim = match rotation {
"rx" | "ry" | "rz" | "p" | "u1" => 2.,
_ => 4.,
};

let trace_over_dim = match rotation {
"rx" | "ry" | "rz" | "rxx" | "ryy" | "rzx" | "rzz" => {
Complex64::new((angle / 2.).cos(), 0.)
}
"crx" | "cry" | "crz" => Complex64::new(0.5 + 0.5 * (angle / 2.).cos(), 0.),
"p" | "u1" => (1. + Complex64::new(0., angle).exp()) / 2.,
"cp" => (3. + Complex64::new(0., angle).exp()) / 4.,
_ => return None,
Copy link
Member

Choose a reason for hiding this comment

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

I feel like we could do this without the strings and used the StandardGate enum variants from standard gates. It would be faster to do the enum match because it's a u8 vs a string comparison.

Copy link
Member

Choose a reason for hiding this comment

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

I just saw this is used elsewhere in accelerate. I feel like this comment is especially true if we're going to use this for standardized trace computation for standard gates.

@@ -14,6 +14,7 @@ use num_complex::ComplexFloat;
use pyo3::prelude::*;
use rustworkx_core::petgraph::stable_graph::NodeIndex;

use crate::commutation_checker::rotation_trace_and_dim;
Copy link
Member

Choose a reason for hiding this comment

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

Do you think we should put this somewhere more central?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I was looking for a more central place but didn't find an existing one that suited well.. and I didn't want to create a new one just with this file. Maybe we can just create a gate_metrics.rs or something that also includes the gate_fidelity function

@@ -33,12 +34,15 @@ fn remove_identity_equiv(
) {
let mut remove_list: Vec<NodeIndex> = Vec::new();
let mut global_phase_update: f64 = 0.;
// Minimum threshold to compare average gate fidelity to 1. This is chosen to account
// for roundoff errors and to be consistent with other places.
let minimum_tol = 1e-12_f64;
Copy link
Member

Choose a reason for hiding this comment

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

I'd make this a const:

Suggested change
let minimum_tol = 1e-12_f64;
const MINIMUM_TOL: f64 = 1e-12;

Although you might have to move it outside the function definition to do this.

let name = gate.name();
if let Param::Float(angle) = inst.params_view()[0] {
let (tr_over_dim, dim) =
rotation_trace_and_dim(name, angle).expect("All rotation covered");
Copy link
Member

Choose a reason for hiding this comment

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

Since this becomes a panic message maybe something a bit more descriptive/clear. We should never encounter it because the match set is complete but since you went to the trouble of adding a message instead of just unwrap() we should provide a bit more detail.

Comment on lines 205 to 210
let mut out = arr2(&[
[C_ZERO, C_ZERO, C_ZERO, C_ZERO],
[C_ZERO, C_ZERO, C_ZERO, C_ZERO],
[C_ZERO, C_ZERO, C_ZERO, C_ZERO],
[C_ZERO, C_ZERO, C_ZERO, C_ZERO],
]);
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason to not do?:

Suggested change
let mut out = arr2(&[
[C_ZERO, C_ZERO, C_ZERO, C_ZERO],
[C_ZERO, C_ZERO, C_ZERO, C_ZERO],
[C_ZERO, C_ZERO, C_ZERO, C_ZERO],
[C_ZERO, C_ZERO, C_ZERO, C_ZERO],
]);
let mut out = Array2::zeros((4, 4));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's zero reason not to 😉

///
/// This is analogous to NumPy's ``allclose`` function.
pub fn allclose(
pub fn mm2q(
Copy link
Member

Choose a reason for hiding this comment

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

Maybe write this out as matmul_2q or something a bit more descriptive? It took me a sec to understand what this meant at first. Or just a docstring to say what it is.

}
}
true
fn mm1q(left: &ArrayView2<Complex64>, right: &ArrayView2<Complex64>) -> Array2<Complex64> {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe write this out as matmul_1q or something a bit more descriptive? It took me a sec to understand what this meant at first then I looked at the code. Or just a docstring to say what it is.

There is also an existing function for this here: https://github.com/Qiskit/qiskit/blob/main/crates/accelerate/src/euler_one_qubit_decomposer.rs#L1233-L1245 but it works with slightly different types.

.. autosummary::
:toctree: ../stubs/

CommutationChecker
Copy link
Member

Choose a reason for hiding this comment

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

Isn't the CommutationChecker part of the public API we probably shouldn't remove this. (we had this exact problem as part of 1.1 or something where somebody removed it from the docs)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

huh the page doesn't exist on the 1.4 docs (which is probably why I removed it here), but it does exist on the 2.0 dev doc -- I'll add it again, thanks for the check!

@@ -0,0 +1,24 @@
---
upgrade:
Copy link
Member

Choose a reason for hiding this comment

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

You need a feature note about the new argument on the commutative pass constructors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

in my mind it was not API documented 😛 I'll add it!

@mtreinish mtreinish added Changelog: New Feature Include in the "Added" section of the changelog Changelog: API Change Include in the "Changed" section of the changelog Changelog: Bugfix Include in the "Fixed" section of the changelog Rust This PR or issue is related to Rust code in the repository labels Mar 5, 2025
@mtreinish mtreinish added the mod: transpiler Issues and PRs related to Transpiler label Mar 5, 2025
Copy link
Member

@mtreinish mtreinish 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 quick updates. There are 3 small nits inline otherwise I think this is ready to go. I left one larger comment inline about to rewrite the rotation gate handling without using a string. But we can save that for a follow up, it's just a pet peeve of mine and is arguably scope creep for this PR.

Comment on lines 56 to 57
let dim = left.nrows(); // == left.ncols() == right.nrows() == right.ncols()
// let trace = left.t().mapv(|el| el.conj()).dot(right).diag().sum();
Copy link
Member

Choose a reason for hiding this comment

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

Are these code comments intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The first lines yes, the second no 😄 but also the first line is not really necessary, I removed them both

@@ -652,13 +605,19 @@ fn map_rotation<'a>(
) -> (&'a OperationRef<'a>, &'a [Param], bool) {
let name = op.name();

if let Some((pi_multiple, generator)) = SUPPORTED_ROTATIONS.get(name) {
if let Some(generator) = SUPPORTED_ROTATIONS.get(name) {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe not for this PR but we can drop the hashset here all together and have a static lookup table based on the standard gates. Something like:

const fn builld_lut() -> [Option<StandardGate>; STANDARD_GATE_SIZE]  {
    ...
}
let static supported_rotations: [Option<StandardGate>; STANDARD_GATE_SIZE] = build_lut()

Alternatively you could just do a couple of if matches!() on .standard_gate().

When in rust space and working solely with standard gates we really never have a need for strings, so this just sticks out to me every time I see it. But it was pre-existing so we can do this in a follow up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah that sounds like a good idea, I'll put it on the follow-up list if that's good for you 🙂

@mtreinish mtreinish enabled auto-merge March 5, 2025 23:09
@mtreinish mtreinish added this pull request to the merge queue Mar 6, 2025
Merged via the queue into Qiskit:main with commit d2f4861 Mar 6, 2025
20 checks passed
@Cryoris Cryoris deleted the gate-fidelity-everywhere branch March 6, 2025 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: API Change Include in the "Changed" section of the changelog Changelog: Bugfix Include in the "Fixed" section of the changelog Changelog: New Feature Include in the "Added" section of the changelog mod: transpiler Issues and PRs related to Transpiler Rust This PR or issue is related to Rust code in the repository
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Incorrect transpilation with RY gate with a small angle
4 participants