Skip to content

Commit 86a1b49

Browse files
raynelfsseliarbelmtreinish
authored
Port circuit_to_dag to Rust (#13036)
* Initial: Add add_from_iter method to DAGCircuit - Introduce a method that adds a chain of `PackedInstruction` continuously avoiding the re-linking of each bit's output-node until the very end of the iterator. - TODO: Add handling of vars - Add header for a `from_iter` function that will create a `DAGCircuit` based on a chain of `PackedInstruction`. * Fix: leverage new methods in layers - Fix incorrect re-insertion of last_node. * Fix: Keep track of Vars for add_from_iter - Remove `from_iter` * Fix: Incorrect modification of last nodes in `add_from_iter`. - Use `entry` api to either modify or insert a value if missing. * Fix: Cycling edges in when adding vars. - A bug that adds duplicate edges to vars has been temporarily fixed. However, the root of this problem hasn't been found yet. A proper fix is pending. For now skip those instances. * Fix: Remove set collecting all nodes to be connected. - A set collecting all the new nodes to connect with a new node was preventing additional wires to connect to subsequent nodes. * Fix: Adapt to #13033 * Refactor: `add_from_iter` is now called `extend` to stick with `Rust` nomenclature. * Fix: Remove duplicate vars check * Fix: Corrections from code review. - Use Entry API to modify last nodes in the var. - Build new_nodes with an allocated vec. - Add comment explaining the removal of the edge between the output node and its predecessor. * Fix: Improper use of `Entry API`. - Use `or_insert_with` instead of `or_insert` to perform actions before inserting a value. * Initial: Add add_from_iter method to DAGCircuit - Introduce a method that adds a chain of `PackedInstruction` continuously avoiding the re-linking of each bit's output-node until the very end of the iterator. - TODO: Add handling of vars - Add header for a `from_iter` function that will create a `DAGCircuit` based on a chain of `PackedInstruction`. * Initial: Expose `CircuitData` interners and registers to the `qiskit-circuit` crate. - Make the `CircuitData` iter method be an exact-size iterator. * FIx: Expose immutable views of interners, registers and global phase. - Revert the changes making the interners and registers visible to the crate `qiskit-circuit`. - Create methods to expose immutable borrowed views of the interners, registers and global_phase to prevent from mutating the DAGCircuit. - Add `get_qargs` and `get_cargs` to unpack interned qargs ans cargs. - Other tweaks and fixes. * Refactor: Use naming convention for getters. * Docs: Apply suggestions from code review - Correct incorrect docstrings for `qubits()` and `clbits()` Co-authored-by: Eli Arbel <[email protected]> * Initial: Add `circuit_to_dag` in rust. - Add new method `DAGCircuit::from_quantum_circuit` which uses the data from a `QuantumCircuit` instance to build a dag_circuit. - Expose the method through a `Python` interface with `circuit_to_dag` which goes by the alias of `core_circuit_to_dag` and is called by the original method. - Add an arbitrary structure `QuantumCircuitData` that successfully extract attributes from the python `QuantumCircuit` instance and makes it easier to access in rust. - This structure is for attribute extraction only and is not clonable/copyable. - Expose a new module `converters` which should store all of the rust-side converters whenever they get brought into rust. - Other small tweaks and fixes. * Fix: Move qubit ordering check to python - Add more accurate estimate of num_edges. * Fix: Regenerate `BitData` instances dynamically instead of cloning. - When converting from `QuantumCircuit` to `DAGCircuit`, we were previously copying the instances of `BitData` by cloning. This would result in instances that had extra qubits that went unused. This commit fixes this oversight and reduced the amount of failing times from 160 to 12. * Fix: Make use of `copy_operations` - Use `copy_operations` to copy all of the operations obtained from `CircuitData` by calling deepcopy. - Initialize `._data` manually for instances of `BlueprintCircuit` by calling `._build()`. - Other small fixes. * FIx: Adapt to 13033 * Fix: Correctly map qubits and clbits to the `DAGCircuit`. - Previously, the binding of qubits and clbits into bitdata was done based on the `push_back()` function behavior. This manual mapping was removed in favor of just inserting the qubits/clbits using the `add_qubits()` and `add_clbits()` methods and keeping track of the new indices. - Remove cloning of interners, use the re-mapped entries instead. - Remove manual re-mapping of qubits and clbits. - Remove cloning of BitData, insert qubits directly instead. - Other small tweaks and fixes. * Add: `DAGCircuit` from `CircuitData` - Make `QuantumCircuitData` extraction struct private. - Rename `DAGCircuit::from_quantum_circuit` into `DAGCircuit::from_circuit` to make more generalized. - Pass all attributes of `QuantumCircuit` that are passed from python as arguments to the `DAGCircuit::from_circuit` function. - Add `DAGCircuit::from_circuit_data` as a wrapper to `from_circuit` to create an instance solely from the properties available in `CircuitData`. - Use `DAGCircuit::from_circuit` as base for `DAGCircuit::from_circuit_data`. * Fix: Make `QuantumCircuitData` public. - `QuantumCircuitData` is an extractor helper built only to extract attributes from a Python-based `Quantumircuit` that are not yet available in rust + the qualities that already are through `CircuitData`. - Add `Clone` trait `QuantumCircuitData` as all its properties are clonable. - Make `circuit_to_dag` public in rust in case it needs to be used for a transpiler pass. - Adapt to renaming of `DAGCircuit::add_from_iter` into `DAGCircuit::extend`. * Fix: Use `intern!` when extracting attributes from Python. * Fix: Copy instructions in place instead of copying circuit data. - Use `QuantumCircuitData` as argument for `DAGCircuit::from_circuit`. - Create instance of `QuantumCircuitData` in `DAGCircuit::from_circuit_data`. - Re-add duplicate skip in extend. - Other tweaks and fixes. * Fix: Remove second re-mapping of bits - Remove second re-mapping of bits, perform this mapping at insertion of qubits instead. - Modify insertion of qubits to re-map the indices and store them after insertion should a custom ordering be provided. - Remove extra `.and_modify()` for `clbits_last_node` from `DAGCircuit::extend`. - Remove submodule addition to `circuit` module, submodule is now under `_accelerate.converters`. - Other tweaks and fixes. * Update crates/circuit/src/dag_circuit.rs Co-authored-by: Matthew Treinish <[email protected]> --------- Co-authored-by: Eli Arbel <[email protected]> Co-authored-by: Matthew Treinish <[email protected]>
1 parent 49b8a5f commit 86a1b49

File tree

6 files changed

+316
-38
lines changed

6 files changed

+316
-38
lines changed

crates/circuit/src/converters.rs

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// This code is part of Qiskit.
2+
//
3+
// (C) Copyright IBM 2023, 2024
4+
//
5+
// This code is licensed under the Apache License, Version 2.0. You may
6+
// obtain a copy of this license in the LICENSE.txt file in the root directory
7+
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
//
9+
// Any modifications or derivative works of this code must retain this
10+
// copyright notice, and modified files need to carry a notice indicating
11+
// that they have been altered from the originals.
12+
13+
use ::pyo3::prelude::*;
14+
use hashbrown::HashMap;
15+
use pyo3::{
16+
intern,
17+
types::{PyDict, PyList},
18+
};
19+
20+
use crate::{circuit_data::CircuitData, dag_circuit::DAGCircuit};
21+
22+
/// An extractable representation of a QuantumCircuit reserved only for
23+
/// conversion purposes.
24+
#[derive(Debug, Clone)]
25+
pub struct QuantumCircuitData<'py> {
26+
pub data: CircuitData,
27+
pub name: Option<Bound<'py, PyAny>>,
28+
pub calibrations: Option<HashMap<String, Py<PyDict>>>,
29+
pub metadata: Option<Bound<'py, PyAny>>,
30+
pub qregs: Option<Bound<'py, PyList>>,
31+
pub cregs: Option<Bound<'py, PyList>>,
32+
pub input_vars: Vec<Bound<'py, PyAny>>,
33+
pub captured_vars: Vec<Bound<'py, PyAny>>,
34+
pub declared_vars: Vec<Bound<'py, PyAny>>,
35+
}
36+
37+
impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> {
38+
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
39+
let py = ob.py();
40+
let circuit_data = ob.getattr("_data")?;
41+
let data_borrowed = circuit_data.extract::<CircuitData>()?;
42+
Ok(QuantumCircuitData {
43+
data: data_borrowed,
44+
name: ob.getattr(intern!(py, "name")).ok(),
45+
calibrations: ob.getattr(intern!(py, "calibrations"))?.extract().ok(),
46+
metadata: ob.getattr(intern!(py, "metadata")).ok(),
47+
qregs: ob
48+
.getattr(intern!(py, "qregs"))
49+
.map(|ob| ob.downcast_into())?
50+
.ok(),
51+
cregs: ob
52+
.getattr(intern!(py, "cregs"))
53+
.map(|ob| ob.downcast_into())?
54+
.ok(),
55+
input_vars: ob
56+
.call_method0(intern!(py, "iter_input_vars"))?
57+
.iter()?
58+
.collect::<PyResult<Vec<_>>>()?,
59+
captured_vars: ob
60+
.call_method0(intern!(py, "iter_captured_vars"))?
61+
.iter()?
62+
.collect::<PyResult<Vec<_>>>()?,
63+
declared_vars: ob
64+
.call_method0(intern!(py, "iter_declared_vars"))?
65+
.iter()?
66+
.collect::<PyResult<Vec<_>>>()?,
67+
})
68+
}
69+
}
70+
71+
#[pyfunction(signature = (quantum_circuit, copy_operations = true, qubit_order = None, clbit_order = None))]
72+
pub fn circuit_to_dag(
73+
py: Python,
74+
quantum_circuit: QuantumCircuitData,
75+
copy_operations: bool,
76+
qubit_order: Option<Vec<Bound<PyAny>>>,
77+
clbit_order: Option<Vec<Bound<PyAny>>>,
78+
) -> PyResult<DAGCircuit> {
79+
DAGCircuit::from_circuit(
80+
py,
81+
quantum_circuit,
82+
copy_operations,
83+
qubit_order,
84+
clbit_order,
85+
)
86+
}
87+
88+
pub fn converters(m: &Bound<PyModule>) -> PyResult<()> {
89+
m.add_function(wrap_pyfunction!(circuit_to_dag, m)?)?;
90+
Ok(())
91+
}

crates/circuit/src/dag_circuit.rs

+208
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ use std::hash::{Hash, Hasher};
1515
use ahash::RandomState;
1616

1717
use crate::bit_data::BitData;
18+
use crate::circuit_data::CircuitData;
1819
use crate::circuit_instruction::{
1920
CircuitInstruction, ExtraInstructionAttributes, OperationFromPython,
2021
};
22+
use crate::converters::QuantumCircuitData;
2123
use crate::dag_node::{DAGInNode, DAGNode, DAGOpNode, DAGOutNode};
2224
use crate::dot_utils::build_dot;
2325
use crate::error::DAGCircuitError;
@@ -6572,7 +6574,12 @@ impl DAGCircuit {
65726574
predecessor_node
65736575
};
65746576

6577+
// Because `DAGCircuit::additional_wires` can return repeated instances of vars,
6578+
// we need to make sure to skip those to avoid cycles.
65756579
vars_last_nodes.set_item(var, new_node.index())?;
6580+
if var_last_node == new_node {
6581+
continue;
6582+
}
65766583
self.dag
65776584
.add_edge(var_last_node, new_node, Wire::Var(var.clone_ref(py)));
65786585
}
@@ -6600,6 +6607,207 @@ impl DAGCircuit {
66006607

66016608
Ok(new_nodes)
66026609
}
6610+
6611+
/// Alternative constructor to build an instance of [DAGCircuit] from a `QuantumCircuit`.
6612+
pub(crate) fn from_circuit(
6613+
py: Python,
6614+
qc: QuantumCircuitData,
6615+
copy_op: bool,
6616+
qubit_order: Option<Vec<Bound<PyAny>>>,
6617+
clbit_order: Option<Vec<Bound<PyAny>>>,
6618+
) -> PyResult<DAGCircuit> {
6619+
// Extract necessary attributes
6620+
let qc_data = qc.data;
6621+
let num_qubits = qc_data.num_qubits();
6622+
let num_clbits = qc_data.num_clbits();
6623+
let num_ops = qc_data.__len__();
6624+
let num_vars = qc.declared_vars.len() + qc.input_vars.len() + qc.captured_vars.len();
6625+
6626+
// Build DAGCircuit with capacity
6627+
let mut new_dag = DAGCircuit::with_capacity(
6628+
py,
6629+
num_qubits,
6630+
num_clbits,
6631+
Some(num_vars),
6632+
Some(num_ops),
6633+
None,
6634+
)?;
6635+
6636+
// Assign other necessary data
6637+
new_dag.name = qc.name.map(|ob| ob.unbind());
6638+
6639+
// Avoid manually acquiring the GIL.
6640+
new_dag.global_phase = match qc_data.global_phase() {
6641+
Param::ParameterExpression(exp) => Param::ParameterExpression(exp.clone_ref(py)),
6642+
Param::Float(float) => Param::Float(*float),
6643+
_ => unreachable!("Incorrect parameter assigned for global phase"),
6644+
};
6645+
6646+
if let Some(calibrations) = qc.calibrations {
6647+
new_dag.calibrations = calibrations;
6648+
}
6649+
6650+
new_dag.metadata = qc.metadata.map(|meta| meta.unbind());
6651+
6652+
// Add the qubits depending on order.
6653+
let qubit_map: Option<Vec<Qubit>> = if let Some(qubit_ordering) = qubit_order {
6654+
let mut ordered_vec = Vec::from_iter((0..num_qubits as u32).map(Qubit));
6655+
qubit_ordering
6656+
.into_iter()
6657+
.try_for_each(|qubit| -> PyResult<()> {
6658+
if new_dag.qubits.find(&qubit).is_some() {
6659+
return Err(DAGCircuitError::new_err(format!(
6660+
"duplicate qubits {}",
6661+
&qubit
6662+
)));
6663+
}
6664+
let qubit_index = qc_data.qubits().find(&qubit).unwrap();
6665+
ordered_vec[qubit_index.0 as usize] =
6666+
new_dag.add_qubit_unchecked(py, &qubit)?;
6667+
Ok(())
6668+
})?;
6669+
Some(ordered_vec)
6670+
} else {
6671+
qc_data
6672+
.qubits()
6673+
.bits()
6674+
.iter()
6675+
.try_for_each(|qubit| -> PyResult<_> {
6676+
new_dag.add_qubit_unchecked(py, qubit.bind(py))?;
6677+
Ok(())
6678+
})?;
6679+
None
6680+
};
6681+
6682+
// Add the clbits depending on order.
6683+
let clbit_map: Option<Vec<Clbit>> = if let Some(clbit_ordering) = clbit_order {
6684+
let mut ordered_vec = Vec::from_iter((0..num_clbits as u32).map(Clbit));
6685+
clbit_ordering
6686+
.into_iter()
6687+
.try_for_each(|clbit| -> PyResult<()> {
6688+
if new_dag.clbits.find(&clbit).is_some() {
6689+
return Err(DAGCircuitError::new_err(format!(
6690+
"duplicate clbits {}",
6691+
&clbit
6692+
)));
6693+
};
6694+
let clbit_index = qc_data.clbits().find(&clbit).unwrap();
6695+
ordered_vec[clbit_index.0 as usize] =
6696+
new_dag.add_clbit_unchecked(py, &clbit)?;
6697+
Ok(())
6698+
})?;
6699+
Some(ordered_vec)
6700+
} else {
6701+
qc_data
6702+
.clbits()
6703+
.bits()
6704+
.iter()
6705+
.try_for_each(|clbit| -> PyResult<()> {
6706+
new_dag.add_clbit_unchecked(py, clbit.bind(py))?;
6707+
Ok(())
6708+
})?;
6709+
None
6710+
};
6711+
6712+
// Add all of the new vars.
6713+
for var in &qc.declared_vars {
6714+
new_dag.add_var(py, var, DAGVarType::Declare)?;
6715+
}
6716+
6717+
for var in &qc.input_vars {
6718+
new_dag.add_var(py, var, DAGVarType::Input)?;
6719+
}
6720+
6721+
for var in &qc.captured_vars {
6722+
new_dag.add_var(py, var, DAGVarType::Capture)?;
6723+
}
6724+
6725+
// Add all the registers
6726+
if let Some(qregs) = qc.qregs {
6727+
for qreg in qregs.iter() {
6728+
new_dag.add_qreg(py, &qreg)?;
6729+
}
6730+
}
6731+
6732+
if let Some(cregs) = qc.cregs {
6733+
for creg in cregs.iter() {
6734+
new_dag.add_creg(py, &creg)?;
6735+
}
6736+
}
6737+
6738+
// Pre-process and re-intern all indices again.
6739+
let instructions: Vec<PackedInstruction> = qc_data
6740+
.iter()
6741+
.map(|instr| -> PyResult<PackedInstruction> {
6742+
// Re-map the qubits
6743+
let new_qargs = if let Some(qubit_mapping) = &qubit_map {
6744+
let qargs = qc_data
6745+
.get_qargs(instr.qubits)
6746+
.iter()
6747+
.map(|bit| qubit_mapping[bit.0 as usize])
6748+
.collect();
6749+
new_dag.qargs_interner.insert_owned(qargs)
6750+
} else {
6751+
new_dag
6752+
.qargs_interner
6753+
.insert(qc_data.get_qargs(instr.qubits))
6754+
};
6755+
// Remap the clbits
6756+
let new_cargs = if let Some(clbit_mapping) = &clbit_map {
6757+
let qargs = qc_data
6758+
.get_cargs(instr.clbits)
6759+
.iter()
6760+
.map(|bit| clbit_mapping[bit.0 as usize])
6761+
.collect();
6762+
new_dag.cargs_interner.insert_owned(qargs)
6763+
} else {
6764+
new_dag
6765+
.cargs_interner
6766+
.insert(qc_data.get_cargs(instr.clbits))
6767+
};
6768+
// Copy the operations
6769+
6770+
Ok(PackedInstruction {
6771+
op: if copy_op {
6772+
instr.op.py_deepcopy(py, None)?
6773+
} else {
6774+
instr.op.clone()
6775+
},
6776+
qubits: new_qargs,
6777+
clbits: new_cargs,
6778+
params: instr.params.clone(),
6779+
extra_attrs: instr.extra_attrs.clone(),
6780+
#[cfg(feature = "cache_pygates")]
6781+
py_op: OnceCell::new(),
6782+
})
6783+
})
6784+
.collect::<PyResult<Vec<_>>>()?;
6785+
6786+
// Finally add all the instructions back
6787+
new_dag.extend(py, instructions)?;
6788+
6789+
Ok(new_dag)
6790+
}
6791+
6792+
/// Builds a [DAGCircuit] based on an instance of [CircuitData].
6793+
pub fn from_circuit_data(
6794+
py: Python,
6795+
circuit_data: CircuitData,
6796+
copy_op: bool,
6797+
) -> PyResult<Self> {
6798+
let circ = QuantumCircuitData {
6799+
data: circuit_data,
6800+
name: None,
6801+
calibrations: None,
6802+
metadata: None,
6803+
qregs: None,
6804+
cregs: None,
6805+
input_vars: Vec::new(),
6806+
captured_vars: Vec::new(),
6807+
declared_vars: Vec::new(),
6808+
};
6809+
Self::from_circuit(py, circ, copy_op, None, None)
6810+
}
66036811
}
66046812

66056813
/// Add to global phase. Global phase can only be Float or ParameterExpression so this

crates/circuit/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
pub mod bit_data;
1414
pub mod circuit_data;
1515
pub mod circuit_instruction;
16+
pub mod converters;
1617
pub mod dag_circuit;
1718
pub mod dag_node;
1819
mod dot_utils;

crates/pyext/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ where
4343
#[pymodule]
4444
fn _accelerate(m: &Bound<PyModule>) -> PyResult<()> {
4545
add_submodule(m, qiskit_circuit::circuit, "circuit")?;
46+
add_submodule(m, qiskit_circuit::converters::converters, "converters")?;
4647
add_submodule(m, qiskit_qasm2::qasm2, "qasm2")?;
4748
add_submodule(m, qiskit_qasm3::qasm3, "qasm3")?;
4849
add_submodule(m, circuit_library, "circuit_library")?;

qiskit/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
# and not have to rely on attribute access. No action needed for top-level extension packages.
6262
sys.modules["qiskit._accelerate.circuit"] = _accelerate.circuit
6363
sys.modules["qiskit._accelerate.circuit_library"] = _accelerate.circuit_library
64+
sys.modules["qiskit._accelerate.converters"] = _accelerate.converters
6465
sys.modules["qiskit._accelerate.convert_2q_block_matrix"] = _accelerate.convert_2q_block_matrix
6566
sys.modules["qiskit._accelerate.dense_layout"] = _accelerate.dense_layout
6667
sys.modules["qiskit._accelerate.error_map"] = _accelerate.error_map

0 commit comments

Comments
 (0)