Skip to content

Add structural equality check for DAGCircuit #14762

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

jakelishman
Copy link
Member

The current implementation of equality (the Python-facing DAGCircuit.__eq__) is a semantic-equality checker. This is typically what users care about. Transpiler-pass authors, however, need to care about determinism in the order of modifications of a DAG, including normally internal details such as the precise set of NodeIndexes that are populated, or the order the edges are traversed for any given node.

This commit adds a new method, DAGCircuit::structurally_equal, which checks the node and edge lists of the graph structure for exact equality (slightly indirectly, since petgraph doesn't let us access the internal details of the StableGraph struct directly), to help verify whether two DAGs have gone through the same order of modifications.

Summary

Details and comments

This needs tests but I wanted to push it for comment before I spend the time writing them, since it's implying strong requirements on determinism for passes. Currently, Rust-space Sabre (since #14317) appears non-deterministic on some ASV benchmarks, but this is because it directly iterates over the internal graph structure (the node and edge lists), which are modified in a non-deterministic order by CommutativeCancellation. The check function in this PR is how I tracked down the non-determinism, using a pretty hacky recipe such as:

import queue
import threading
from qiskit import transpile, QuantumCircuit

cm = [
    [0, 5], [0, 1], [1, 2], [1, 0], [2, 3], [2, 1], [3, 4], [3, 2], [4, 6], [4, 3], [5, 9], [5, 0], [6, 13], [6, 4], [7, 16],
    [7, 8], [8, 9], [8, 7], [9, 10], [9, 8], [9, 5], [10, 11], [10, 9], [11, 17], [11, 12], [11, 10], [12, 13], [12, 11], [13, 14],
    [13, 12], [13, 6], [14, 15], [14, 13], [15, 18], [15, 14], [16, 19], [16, 7], [17, 23], [17, 11], [18, 27], [18, 15], [19, 20],
    [19, 16], [20, 21], [20, 19], [21, 28], [21, 22], [21, 20], [22, 23], [22, 21], [23, 24], [23, 22], [23, 17], [24, 25], [24, 23],
    [25, 29], [25, 26], [25, 24], [26, 27], [26, 25], [27, 26], [27, 18], [28, 32], [28, 21], [29, 36], [29, 25], [30, 39], [30, 31],
    [31, 32], [31, 30], [32, 33], [32, 31], [32, 28], [33, 34], [33, 32], [34, 40], [34, 35], [34, 33], [35, 36], [35, 34], [36, 37],
    [36, 35], [36, 29], [37, 38], [37, 36], [38, 41], [38, 37], [39, 42], [39, 30], [40, 46], [40, 34], [41, 50], [41, 38], [42, 43],
    [42, 39], [43, 44], [43, 42], [44, 51], [44, 45], [44, 43], [45, 46], [45, 44], [46, 47], [46, 45], [46, 40], [47, 48], [47, 46],
    [48, 52], [48, 49], [48, 47], [49, 50], [49, 48], [50, 49], [50, 41], [51, 44], [52, 48],
]
# (assuming we're in the repo root)
qc = QuantumCircuit.from_qasm_file("test/benchmarks/qasm/53QBT_100CYC_QSE_3.qasm")

kwargs = dict(
    coupling_map=cm,
    basis_gates=["id", "rz", "sx", "x", "cx"],
    layout_method="sabre",
    optimization_level=2,
    seed_transpiler=0,
)

def per_pass_iter(qc, kwargs):
    worker = queue.Queue(1)
    driver = queue.Queue(1)
    done = object()
    keep_going = object()

    def run_transpile():
        def push(pass_, dag, **_):
            driver.put((pass_, dag))
            if worker.get() is done:
                raise SystemExit

        if worker.get() is done:
            return
        transpile(qc, **kwargs, callback=push)
        driver.put(done)

    thread = threading.Thread(target=run_transpile)
    thread.start()

    try:
        while True:
            worker.put(keep_going)
            if (msg := driver.get()) is done:
                return
            yield msg
    finally:
        worker.put(done)

for (left_pass, left), (right_pass, right) in zip(per_pass_iter(qc, kwargs), per_pass_iter(qc, kwargs)):
    assert left_pass.name() == right_pass.name()
    print(left_pass.name(), left.structurally_equal(right))

The current implementation of equality (the Python-facing
`DAGCircuit.__eq__`) is a semantic-equality checker.  This is typically
what users care about.  Transpiler-pass authors, however, need to care
about determinism in the order of modifications of a DAG, including
normally internal details such as the precise set of `NodeIndex`es that
are populated, or the order the edges are traversed for any given node.

This commit adds a new method, `DAGCircuit::structurally_equal`, which
checks the node and edge lists of the graph structure for exact equality
(slightly indirectly, since `petgraph` doesn't let us access the
internal details of the `StableGraph` struct directly), to help verify
whether two DAGs have gone through the same order of modifications.
@jakelishman jakelishman added this to the 2.2.0 milestone Jul 19, 2025
@jakelishman jakelishman added Changelog: New Feature Include in the "Added" section of the changelog mod: transpiler Issues and PRs related to Transpiler labels Jul 19, 2025
@coveralls
Copy link

Pull Request Test Coverage Report for Build 16383443779

Details

  • 0 of 128 (0.0%) changed or added relevant lines in 3 files are covered.
  • 5 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-0.1%) to 87.663%

Changes Missing Coverage Covered Lines Changed/Added Lines %
crates/circuit/src/object_registry.rs 0 3 0.0%
crates/circuit/src/register_data.rs 0 3 0.0%
crates/circuit/src/dag_circuit.rs 0 122 0.0%
Files with Coverage Reduction New Missed Lines %
crates/circuit/src/symbol_expr.rs 1 73.98%
crates/qasm2/src/lex.rs 4 92.53%
Totals Coverage Status
Change from base Build 16327707788: -0.1%
Covered Lines: 81492
Relevant Lines: 92961

💛 - Coveralls

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 mod: transpiler Issues and PRs related to Transpiler
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants