Skip to content

Commit

Permalink
Port circuit_to_dag to Rust (#13036)
Browse files Browse the repository at this point in the history
* 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 <46826214+eliarbel@users.noreply.github.com>

* 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 <mtreinish@kortar.org>

---------

Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com>
Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
  • Loading branch information
3 people authored Sep 10, 2024
1 parent 49b8a5f commit 86a1b49
Show file tree
Hide file tree
Showing 6 changed files with 316 additions and 38 deletions.
91 changes: 91 additions & 0 deletions crates/circuit/src/converters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2023, 2024
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.

use ::pyo3::prelude::*;
use hashbrown::HashMap;
use pyo3::{
intern,
types::{PyDict, PyList},
};

use crate::{circuit_data::CircuitData, dag_circuit::DAGCircuit};

/// An extractable representation of a QuantumCircuit reserved only for
/// conversion purposes.
#[derive(Debug, Clone)]
pub struct QuantumCircuitData<'py> {
pub data: CircuitData,
pub name: Option<Bound<'py, PyAny>>,
pub calibrations: Option<HashMap<String, Py<PyDict>>>,
pub metadata: Option<Bound<'py, PyAny>>,
pub qregs: Option<Bound<'py, PyList>>,
pub cregs: Option<Bound<'py, PyList>>,
pub input_vars: Vec<Bound<'py, PyAny>>,
pub captured_vars: Vec<Bound<'py, PyAny>>,
pub declared_vars: Vec<Bound<'py, PyAny>>,
}

impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> {
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
let py = ob.py();
let circuit_data = ob.getattr("_data")?;
let data_borrowed = circuit_data.extract::<CircuitData>()?;
Ok(QuantumCircuitData {
data: data_borrowed,
name: ob.getattr(intern!(py, "name")).ok(),
calibrations: ob.getattr(intern!(py, "calibrations"))?.extract().ok(),
metadata: ob.getattr(intern!(py, "metadata")).ok(),
qregs: ob
.getattr(intern!(py, "qregs"))
.map(|ob| ob.downcast_into())?
.ok(),
cregs: ob
.getattr(intern!(py, "cregs"))
.map(|ob| ob.downcast_into())?
.ok(),
input_vars: ob
.call_method0(intern!(py, "iter_input_vars"))?
.iter()?
.collect::<PyResult<Vec<_>>>()?,
captured_vars: ob
.call_method0(intern!(py, "iter_captured_vars"))?
.iter()?
.collect::<PyResult<Vec<_>>>()?,
declared_vars: ob
.call_method0(intern!(py, "iter_declared_vars"))?
.iter()?
.collect::<PyResult<Vec<_>>>()?,
})
}
}

#[pyfunction(signature = (quantum_circuit, copy_operations = true, qubit_order = None, clbit_order = None))]
pub fn circuit_to_dag(
py: Python,
quantum_circuit: QuantumCircuitData,
copy_operations: bool,
qubit_order: Option<Vec<Bound<PyAny>>>,
clbit_order: Option<Vec<Bound<PyAny>>>,
) -> PyResult<DAGCircuit> {
DAGCircuit::from_circuit(
py,
quantum_circuit,
copy_operations,
qubit_order,
clbit_order,
)
}

pub fn converters(m: &Bound<PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(circuit_to_dag, m)?)?;
Ok(())
}
208 changes: 208 additions & 0 deletions crates/circuit/src/dag_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ use std::hash::{Hash, Hasher};
use ahash::RandomState;

use crate::bit_data::BitData;
use crate::circuit_data::CircuitData;
use crate::circuit_instruction::{
CircuitInstruction, ExtraInstructionAttributes, OperationFromPython,
};
use crate::converters::QuantumCircuitData;
use crate::dag_node::{DAGInNode, DAGNode, DAGOpNode, DAGOutNode};
use crate::dot_utils::build_dot;
use crate::error::DAGCircuitError;
Expand Down Expand Up @@ -6572,7 +6574,12 @@ impl DAGCircuit {
predecessor_node
};

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

Ok(new_nodes)
}

/// Alternative constructor to build an instance of [DAGCircuit] from a `QuantumCircuit`.
pub(crate) fn from_circuit(
py: Python,
qc: QuantumCircuitData,
copy_op: bool,
qubit_order: Option<Vec<Bound<PyAny>>>,
clbit_order: Option<Vec<Bound<PyAny>>>,
) -> PyResult<DAGCircuit> {
// Extract necessary attributes
let qc_data = qc.data;
let num_qubits = qc_data.num_qubits();
let num_clbits = qc_data.num_clbits();
let num_ops = qc_data.__len__();
let num_vars = qc.declared_vars.len() + qc.input_vars.len() + qc.captured_vars.len();

// Build DAGCircuit with capacity
let mut new_dag = DAGCircuit::with_capacity(
py,
num_qubits,
num_clbits,
Some(num_vars),
Some(num_ops),
None,
)?;

// Assign other necessary data
new_dag.name = qc.name.map(|ob| ob.unbind());

// Avoid manually acquiring the GIL.
new_dag.global_phase = match qc_data.global_phase() {
Param::ParameterExpression(exp) => Param::ParameterExpression(exp.clone_ref(py)),
Param::Float(float) => Param::Float(*float),
_ => unreachable!("Incorrect parameter assigned for global phase"),
};

if let Some(calibrations) = qc.calibrations {
new_dag.calibrations = calibrations;
}

new_dag.metadata = qc.metadata.map(|meta| meta.unbind());

// Add the qubits depending on order.
let qubit_map: Option<Vec<Qubit>> = if let Some(qubit_ordering) = qubit_order {
let mut ordered_vec = Vec::from_iter((0..num_qubits as u32).map(Qubit));
qubit_ordering
.into_iter()
.try_for_each(|qubit| -> PyResult<()> {
if new_dag.qubits.find(&qubit).is_some() {
return Err(DAGCircuitError::new_err(format!(
"duplicate qubits {}",
&qubit
)));
}
let qubit_index = qc_data.qubits().find(&qubit).unwrap();
ordered_vec[qubit_index.0 as usize] =
new_dag.add_qubit_unchecked(py, &qubit)?;
Ok(())
})?;
Some(ordered_vec)
} else {
qc_data
.qubits()
.bits()
.iter()
.try_for_each(|qubit| -> PyResult<_> {
new_dag.add_qubit_unchecked(py, qubit.bind(py))?;
Ok(())
})?;
None
};

// Add the clbits depending on order.
let clbit_map: Option<Vec<Clbit>> = if let Some(clbit_ordering) = clbit_order {
let mut ordered_vec = Vec::from_iter((0..num_clbits as u32).map(Clbit));
clbit_ordering
.into_iter()
.try_for_each(|clbit| -> PyResult<()> {
if new_dag.clbits.find(&clbit).is_some() {
return Err(DAGCircuitError::new_err(format!(
"duplicate clbits {}",
&clbit
)));
};
let clbit_index = qc_data.clbits().find(&clbit).unwrap();
ordered_vec[clbit_index.0 as usize] =
new_dag.add_clbit_unchecked(py, &clbit)?;
Ok(())
})?;
Some(ordered_vec)
} else {
qc_data
.clbits()
.bits()
.iter()
.try_for_each(|clbit| -> PyResult<()> {
new_dag.add_clbit_unchecked(py, clbit.bind(py))?;
Ok(())
})?;
None
};

// Add all of the new vars.
for var in &qc.declared_vars {
new_dag.add_var(py, var, DAGVarType::Declare)?;
}

for var in &qc.input_vars {
new_dag.add_var(py, var, DAGVarType::Input)?;
}

for var in &qc.captured_vars {
new_dag.add_var(py, var, DAGVarType::Capture)?;
}

// Add all the registers
if let Some(qregs) = qc.qregs {
for qreg in qregs.iter() {
new_dag.add_qreg(py, &qreg)?;
}
}

if let Some(cregs) = qc.cregs {
for creg in cregs.iter() {
new_dag.add_creg(py, &creg)?;
}
}

// Pre-process and re-intern all indices again.
let instructions: Vec<PackedInstruction> = qc_data
.iter()
.map(|instr| -> PyResult<PackedInstruction> {
// Re-map the qubits
let new_qargs = if let Some(qubit_mapping) = &qubit_map {
let qargs = qc_data
.get_qargs(instr.qubits)
.iter()
.map(|bit| qubit_mapping[bit.0 as usize])
.collect();
new_dag.qargs_interner.insert_owned(qargs)
} else {
new_dag
.qargs_interner
.insert(qc_data.get_qargs(instr.qubits))
};
// Remap the clbits
let new_cargs = if let Some(clbit_mapping) = &clbit_map {
let qargs = qc_data
.get_cargs(instr.clbits)
.iter()
.map(|bit| clbit_mapping[bit.0 as usize])
.collect();
new_dag.cargs_interner.insert_owned(qargs)
} else {
new_dag
.cargs_interner
.insert(qc_data.get_cargs(instr.clbits))
};
// Copy the operations

Ok(PackedInstruction {
op: if copy_op {
instr.op.py_deepcopy(py, None)?
} else {
instr.op.clone()
},
qubits: new_qargs,
clbits: new_cargs,
params: instr.params.clone(),
extra_attrs: instr.extra_attrs.clone(),
#[cfg(feature = "cache_pygates")]
py_op: OnceCell::new(),
})
})
.collect::<PyResult<Vec<_>>>()?;

// Finally add all the instructions back
new_dag.extend(py, instructions)?;

Ok(new_dag)
}

/// Builds a [DAGCircuit] based on an instance of [CircuitData].
pub fn from_circuit_data(
py: Python,
circuit_data: CircuitData,
copy_op: bool,
) -> PyResult<Self> {
let circ = QuantumCircuitData {
data: circuit_data,
name: None,
calibrations: None,
metadata: None,
qregs: None,
cregs: None,
input_vars: Vec::new(),
captured_vars: Vec::new(),
declared_vars: Vec::new(),
};
Self::from_circuit(py, circ, copy_op, None, None)
}
}

/// Add to global phase. Global phase can only be Float or ParameterExpression so this
Expand Down
1 change: 1 addition & 0 deletions crates/circuit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
pub mod bit_data;
pub mod circuit_data;
pub mod circuit_instruction;
pub mod converters;
pub mod dag_circuit;
pub mod dag_node;
mod dot_utils;
Expand Down
1 change: 1 addition & 0 deletions crates/pyext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ where
#[pymodule]
fn _accelerate(m: &Bound<PyModule>) -> PyResult<()> {
add_submodule(m, qiskit_circuit::circuit, "circuit")?;
add_submodule(m, qiskit_circuit::converters::converters, "converters")?;
add_submodule(m, qiskit_qasm2::qasm2, "qasm2")?;
add_submodule(m, qiskit_qasm3::qasm3, "qasm3")?;
add_submodule(m, circuit_library, "circuit_library")?;
Expand Down
1 change: 1 addition & 0 deletions qiskit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
# and not have to rely on attribute access. No action needed for top-level extension packages.
sys.modules["qiskit._accelerate.circuit"] = _accelerate.circuit
sys.modules["qiskit._accelerate.circuit_library"] = _accelerate.circuit_library
sys.modules["qiskit._accelerate.converters"] = _accelerate.converters
sys.modules["qiskit._accelerate.convert_2q_block_matrix"] = _accelerate.convert_2q_block_matrix
sys.modules["qiskit._accelerate.dense_layout"] = _accelerate.dense_layout
sys.modules["qiskit._accelerate.error_map"] = _accelerate.error_map
Expand Down
Loading

0 comments on commit 86a1b49

Please sign in to comment.