Skip to content

Commit 101ff10

Browse files
kevinhartmanmtreinishraynelfssElePTalexanderivrii
committed
Port DAGCircuit to Rust
This commit migrates the entirety of the `DAGCircuit` class to Rust. It fully replaces the Python version of the class. The primary advantage of this migration is moving from a Python space rustworkx directed graph representation to a Rust space petgraph (the upstream library for rustworkx) directed graph. Moving the graph data structure to rust enables us to directly interact with the DAG directly from transpiler passes in Rust in the future. This will enable a significant speed-up in those transpiler passes. Additionally, this should also improve the memory footprint as the DAGCircuit no longer stores `DAGNode` instances, and instead stores a lighter enum NodeType, which simply contains a `PackedInstruction` or the wire objects directly. Internally, the new Rust-based `DAGCircuit` uses a `petgraph::StableGraph` with node weights of type `NodeType` and edge weights of type `Wire`. The NodeType enum contains variants for `QubitIn`, `QubitOut`, `ClbitIn`, `ClbitOut`, and `Operation`, which should save us from all of the `isinstance` checking previously needed when working with `DAGNode` Python instances. The `Wire` enum contains variants `Qubit`, `Clbit`, and `Var`. As the full Qiskit data model is not rust-native at this point while all the class code in the `DAGCircuit` exists in Rust now, there are still sections that rely on Python or actively run Python code via Rust to function. These typically involve anything that uses `condition`, control flow, classical vars, calibrations, bit/register manipulation, etc. In the future as we either migrate this functionality to Rust or deprecate and remove it this can be updated in place to avoid the use of Python. API access from Python-space remains in terms of `DAGNode` instances to maintain API compatibility with the Python implementation. However, internally, we convert to and deal in terms of NodeType. When the user requests a particular node via lookup or iteration, we inflate an ephemeral `DAGNode` based on the internal `NodeType` and give them that. This is very similar to what was done in Qiskit#10827 when porting CircuitData to Rust. As part of this porting there are a few small differences to keep in mind with the new Rust implementation of DAGCircuit. The first is that the topological ordering is slightly different with the new DAGCircuit. Previously, the Python version of `DAGCircuit` using a lexicographical topological sort key which was basically `"0,1,0,2"` where the first `0,1` are qargs on qubit indices `0,1` for nodes and `0,2` are cargs on clbit indices `0,2`. However, the sort key has now changed to be `(&[Qubit(0), Qubit(1)], &[Clbit(0), Clbit(2)])` in rust in this case which for the most part should behave identically, but there are some edge cases that will appear where the sort order is different. It will always be a valid topological ordering as the lexicographical key is used as a tie breaker when generating a topological sort. But if you're relaying on the exact same sort order there will be differences after this PR. The second is that a lot of undocumented functionality in the DAGCircuit which previously worked because of Python's implicit support for interacting with data structures is no longer functional. For example, previously the `DAGCircuit.qubits` list could be set directly (as the circuit visualizers previously did), but this was never documented as supported (and would corrupt the DAGCircuit). Any functionality like this we'd have to explicit include in the Rust implementation and as they were not included in the documented public API this PR opted to remove the vast majority of this type of functionality. The last related thing might require future work to mitigate is that this PR breaks the linkage between `DAGNode` and the underlying `DAGCirucit` object. In the Python implementation the `DAGNode` objects were stored directly in the `DAGCircuit` and when an API method returned a `DAGNode` from the DAG it was a shared reference to the underlying object in the `DAGCircuit`. This meant if you mutated the `DAGNode` it would be reflected in the `DAGCircuit`. This was not always a sound usage of the API as the `DAGCircuit` was implicitly caching many attributes of the DAG and you should always be using the `DAGCircuit` API to mutate any nodes to prevent any corruption of the `DAGCircuit`. However, now as the underlying data store for nodes in the DAG are no longer the python space objects returned by `DAGCircuit` methods mutating a `DAGNode` will not make any change in the underlying `DAGCircuit`. This can come as quite the surprise at first, especially if you were relying on this side effect, even if it was unsound. It's also worth noting that 2 large pieces of functionality from rustworkx are included in this PR. These are the new files `rustworkx_core_vnext` and `dot_utils` which are rustworkx's VF2 implementation and its dot file generation. As there was not a rust interface exposed for this functionality from rustworkx-core there was no way to use these functions in rustworkx. Until these interfaces added to rustworkx-core in future releases we'll have to keep these local copies. The vf2 implementation is in progress in Qiskit/rustworkx#1235, but `dot_utils` might make sense to keep around longer term as it is slightly modified from the upstream rustworkx implementation to directly interface with `DAGCircuit` instead of a generic graph. Co-authored-by: Matthew Treinish <[email protected]> Co-authored-by: Raynel Sanchez <[email protected]> Co-authored-by: Elena Peña Tapia <[email protected]> Co-authored-by: Alexander Ivrii <[email protected]> Co-authored-by: Eli Arbel <[email protected]> Co-authored-by: John Lapeyre <[email protected]> Co-authored-by: Jake Lishman <[email protected]>
1 parent d3040a0 commit 101ff10

File tree

12 files changed

+961
-689
lines changed

12 files changed

+961
-689
lines changed

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ license = "Apache-2.0"
1414
#
1515
# Each crate can add on specific features freely as it inherits.
1616
[workspace.dependencies]
17-
bytemuck = "1.17"
18-
indexmap.version = "2.4.0"
17+
bytemuck = "1.16"
18+
indexmap.version = "2.3.0"
1919
hashbrown.version = "0.14.5"
2020
num-bigint = "0.4"
2121
num-complex = "0.4"

crates/circuit/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ name = "qiskit_circuit"
1010
doctest = false
1111

1212
[dependencies]
13-
rayon.workspace = true
13+
rayon = "1.10"
1414
ahash.workspace = true
1515
rustworkx-core.workspace = true
1616
bytemuck.workspace = true

crates/circuit/src/bit_data.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use pyo3::prelude::*;
1717
use pyo3::types::PyList;
1818
use std::fmt::Debug;
1919
use std::hash::{Hash, Hasher};
20+
use std::mem::swap;
2021

2122
/// Private wrapper for Python-side Bit instances that implements
2223
/// [Hash] and [Eq], allowing them to be used in Rust hash-based
@@ -220,3 +221,37 @@ where
220221
self.bits.clear();
221222
}
222223
}
224+
225+
pub struct Iter<'a, T> {
226+
_data: &'a BitData<T>,
227+
index: usize,
228+
}
229+
230+
impl<'a, T> Iterator for Iter<'a, T>
231+
where
232+
T: From<BitType>,
233+
{
234+
type Item = T;
235+
236+
fn next(&mut self) -> Option<Self::Item> {
237+
let mut index = self.index + 1;
238+
swap(&mut self.index, &mut index);
239+
let index: Option<BitType> = index.try_into().ok();
240+
index.map(|i| From::from(i))
241+
}
242+
}
243+
244+
impl<'a, T> IntoIterator for &'a BitData<T>
245+
where
246+
T: From<BitType>,
247+
{
248+
type Item = T;
249+
type IntoIter = Iter<'a, T>;
250+
251+
fn into_iter(self) -> Self::IntoIter {
252+
Iter {
253+
_data: self,
254+
index: 0,
255+
}
256+
}
257+
}

0 commit comments

Comments
 (0)