Skip to content

Unify identifer handling of Var and Stretch in DAGCircuit #14000

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

Merged
merged 20 commits into from
Apr 10, 2025

Conversation

kevinhartman
Copy link
Contributor

@kevinhartman kevinhartman commented Mar 11, 2025

Summary

During testing, I've found two bugs with DAGCircuit's handling of stretches:

  1. Stretch capture order should not matter in circuit equality #13998
  2. DAGCircuit does not properly pickle stretches #13999

They are fixed here.

Details and comments

I've also reworked DAGCircuit's tracking of stretches to be the same as what we do for vars. Namely, there's now just an identifier_info field (which replaces var_info) which makes it easier to detect name collisions between all identifiers in tracking, and also should make it easier to support native handling of stretches in the future.

To make things a bit cleaner, I've moved Var to lib.rs and defined a macro impl_circuit_identifier to reduce boilerplate between our Qubit, Clbit, Var, and now Stretch identifier types.

I've also moved the logic of pickling identifier info tracking from __getstate__ and __setstate__ to FromPyObject and IntoPyObject implementations.

To-do

  • It looks to me like we don't have any testing of pickling / unpickling a DAGCircuit. Can that possibly be true?

This is only really a user-facing error message when
working with DAGCircuit, since QuantumCircuit first
checks if the bits being added to it are duplicates.
And, in the case of DAGCircuit, the previous error
message was already unfriendly:

ValueError: Existing bit ShareableQubit(Owned { register: OwningRegisterInfo { name: "q16", size: 2, subclass: QUBIT }, index: 0 }) cannot be re-added in strict mode.
Tracks stretches the same way we track vars.
Also happens to fix a bug in DAG equality where
order mattered between stretch captures (it should
never have). And, fixes a serialization bug with
stretches.
@kevinhartman kevinhartman added the Changelog: None Do not include in changelog label Mar 11, 2025
@kevinhartman kevinhartman added this to the 2.0.0 milestone Mar 11, 2025
@kevinhartman kevinhartman self-assigned this Mar 11, 2025
@kevinhartman kevinhartman requested a review from a team as a code owner March 11, 2025 21:57
@qiskit-bot
Copy link
Collaborator

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

  • @Qiskit/terra-core

@mtreinish mtreinish added the stable backport potential The bug might be minimal and/or import enough to be port to stable label Mar 11, 2025
@coveralls
Copy link

coveralls commented Mar 11, 2025

Pull Request Test Coverage Report for Build 14337982611

Details

  • 206 of 231 (89.18%) changed or added relevant lines in 3 files are covered.
  • 5 unchanged lines in 2 files lost coverage.
  • Overall coverage increased (+0.06%) to 88.218%

Changes Missing Coverage Covered Lines Changed/Added Lines %
crates/circuit/src/lib.rs 13 17 76.47%
crates/circuit/src/dag_circuit.rs 191 212 90.09%
Files with Coverage Reduction New Missed Lines %
crates/circuit/src/dag_circuit.rs 1 86.69%
crates/qasm2/src/lex.rs 4 91.73%
Totals Coverage Status
Change from base Build 14322076502: 0.06%
Covered Lines: 73590
Relevant Lines: 83418

💛 - Coveralls

@mtreinish
Copy link
Member

It looks to me like we don't have any testing of pickling / unpickling a DAGCircuit. Can that possibly be true?

If we have any we get it indirectly through tests that are using multiprocessing (like transpiling > 1 circuit) because pickle will be used for IPC or deep copy which also internally uses pickle. That being said see #13980 for some explicit tests I had to add for another pickle bug that's been around since 1.3 (when we ported DAGCircuit to rust).

@kevinhartman
Copy link
Contributor Author

kevinhartman commented Mar 12, 2025

see #13980 for some explicit tests I had to add for another pickle bug that's been around since 1.3 (when we ported DAGCircuit to rust).

Great, thanks! I've added a few tests based on those here to validate that stretches are preserved after pickling and deepcopy.

I also tested vars locally, but I found that there's a bug there that predates 2.0 so those tests aren't added in this PR (we ought to address that separately):

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.

I know you've got a pending rebase coming so let me just leave my comments from part way through the review and I'll look again after the rebase is up. That being said I'm wondering if we should concentrate on a dedicated 2.0 fix in a direct PR on stable/2.0 and leave the larger changes in this PR for just 2.1?

@@ -2206,8 +2269,8 @@ impl DAGCircuit {
/// but was changed by issue #2564 to return number of qubits + clbits
/// with the new function DAGCircuit.num_qubits replacing the former
/// semantic of DAGCircuit.width().
fn width(&self) -> usize {
self.qubits.len() + self.clbits.len() + self.vars_info.len()
fn width(&self, py: Python) -> usize {
Copy link
Member

Choose a reason for hiding this comment

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

I assume this is something that won't be required after #14068 is done?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should be possible, though it's not clear to me if it should happen in that PR or a follow-up. That PR (so far) ports the Python Expr and Type hierarchies to Rust, but does not yet change over how we track things in the circuit.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah as we discussed offline I think it's fine to do this in a follow up after #14068.

I'm just mildly concerned that this is adding a deeper dependency on Python for something we should be able to figure out structurally from the dag without Python.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. We do have this information in a way that doesn't need Python as it is, but it'd be inefficient since we'd have to walk over all of the DAGIdentifierInfos and count which ones are variables.

I will find some faster solution in a follow-up that ditches use of the Python token.

Comment on lines 4442 to 4443
/// stretch: the stretch to add.
fn add_captured_stretch(&mut self, py: Python, stretch: &Bound<PyAny>) -> PyResult<()> {
Copy link
Member

Choose a reason for hiding this comment

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

This is actually a breaking api change right? Python's arguments by default are both positional and by name, and we didn't declare the signature of this pymethod to just deal with this positionally the name can be used to do dag.add_captured_stretch(var=stretch) which would be broken by this change. So I don't think we can do this change until 3.0 since we released 2.0.0rc1 and rc2 with a fixed API surface we're stuck with this name for now.

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 was originally hoping this PR would land earlier, but I've renamed stretch to var in both signatures for now: 7bba590

if let Some(previous) = self.declared_stretches.get(&var_name) {
if var.eq(previous)? {
/// stretch: the stretch to add.
fn add_declared_stretch(&mut self, py: Python, stretch: &Bound<PyAny>) -> PyResult<()> {
Copy link
Member

Choose a reason for hiding this comment

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

This is the same problem with this being a breaking change

Comment on lines 4547 to 4562
#[getter]
fn num_stretches(&self, py: Python) -> usize {
self.num_captured_stretches(py) + self.num_declared_stretches(py)
}

/// Number of captured stretches tracked by the circuit.
#[getter]
fn num_captured_stretches(&self, py: Python) -> usize {
self.captured_stretches.bind(py).len()
}

/// Number of declared local stretches tracked by the circuit.
#[getter]
fn num_declared_stretches(&self, py: Python) -> usize {
self.declared_stretches.bind(py).len()
}
Copy link
Member

Choose a reason for hiding this comment

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

If this is for 2.0 I don't think we can backport this since these are new attributes for the dag. This would only be for 2.1. They make total sense to add, but we might want to split this PR out into multiple parts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. I've removed the backport label and will add a release note for this PR for 2.1.

@kevinhartman kevinhartman removed the stable backport potential The bug might be minimal and/or import enough to be port to stable label Mar 27, 2025
@kevinhartman
Copy link
Contributor Author

@mtreinish

That being said I'm wondering if we should concentrate on a dedicated 2.0 fix in a direct PR on stable/2.0 and leave the larger changes in this PR for just 2.1?

That sounds good. I've removed the backport label from this PR and will open a new one against stable/2.0 that takes the tests from here and fixes things via other means.

@kevinhartman kevinhartman added Changelog: New Feature Include in the "Added" section of the changelog mod: transpiler Issues and PRs related to Transpiler labels Mar 27, 2025
@mtreinish mtreinish modified the milestones: 2.0.0, 2.1.0 Mar 27, 2025
@kevinhartman kevinhartman changed the title Fix equality and pickling of DAGCircuit with stretches Unify identifer handling of Var and Stretch in DAGCircuit Apr 1, 2025
@@ -2206,8 +2269,8 @@ impl DAGCircuit {
/// but was changed by issue #2564 to return number of qubits + clbits
/// with the new function DAGCircuit.num_qubits replacing the former
/// semantic of DAGCircuit.width().
fn width(&self) -> usize {
self.qubits.len() + self.clbits.len() + self.vars_info.len()
fn width(&self, py: Python) -> usize {
Copy link
Member

Choose a reason for hiding this comment

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

Yeah as we discussed offline I think it's fine to do this in a follow up after #14068.

I'm just mildly concerned that this is adding a deeper dependency on Python for something we should be able to figure out structurally from the dag without Python.

Comment on lines +446 to +447
fn from_pickle(ob: &Bound<PyAny>) -> PyResult<Self> {
let val_tuple = ob.downcast::<PyTuple>()?;
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 just do?:

Suggested change
fn from_pickle(ob: &Bound<PyAny>) -> PyResult<Self> {
let val_tuple = ob.downcast::<PyTuple>()?;
fn from_pickle(val_tuple: &Bound<PyTuple>) -> PyResult<Self> {

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 think just to be consistent with the other from_pickle methods, really. There isn't any auto-extraction possible here anyway, since we store this all in a PyDict which, when iterated over, yields PyAny pairs.

Comment on lines +47 to +50
pub struct Var(u32);

#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct Stretch(u32);
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 make the inner u32 pub 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.

I didn't make it public since it doesn't have real meaning, at least compared to the int of a Qubit or Clbit.

@@ -37,53 +37,60 @@ use pyo3::prelude::*;
use pyo3::types::{PySequence, PyTuple};
use pyo3::PyTypeInfo;

pub type BitType = u32;
Copy link
Member

Choose a reason for hiding this comment

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

Is the reason to remove this because it's used for more than bits now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pretty much, yeah.

I'd introduced it originally, but it doesn't feel very Rust-y. I think it wasn't doing much for us.

@@ -10,6 +10,8 @@
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

# pylint: disable=invalid-name
Copy link
Member

Choose a reason for hiding this comment

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

What's the invalid name that pylint was complaining about?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is, I believe, because of the additional letter-only names I used. Like "c" and "d".

There were already letter-only names, so I figured this should be allowed. Let me know if you have other suggestions :)

@kevinhartman kevinhartman requested a review from mtreinish April 10, 2025 18:37
@mtreinish mtreinish added this pull request to the merge queue Apr 10, 2025
Merged via the queue into Qiskit:main with commit 8ec4c9c Apr 10, 2025
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: New Feature Include in the "Added" section of the changelog Changelog: None Do not include in changelog mod: transpiler Issues and PRs related to Transpiler
Projects
None yet
Development

Successfully merging this pull request may close these issues.

DAGCircuit does not properly pickle stretches Stretch capture order should not matter in circuit equality
4 participants