Skip to content

Commit

Permalink
Initial: Add circuit_to_dag in rust.
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
raynelfss committed Aug 26, 2024
1 parent af55788 commit 7aa6d71
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 40 deletions.
69 changes: 69 additions & 0 deletions crates/circuit/src/converters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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::types::{PyDict, PyList};

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

/// An extractable representation of a QuantumCircuit reserved only for
/// conversion purposes.
///
/// ## Notes:
/// This structure does not implement `Clone`, this is the intended behavior as
/// it contains callbacks to Python and should not be stored anywhere.
#[derive(Debug)]
pub(crate) struct QuantumCircuitData<'py> {
pub data: PyRef<'py, CircuitData>,
pub name: Option<Bound<'py, PyAny>>,
pub calibrations: 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: Option<Bound<'py, PyAny>>,
pub captured_vars: Option<Bound<'py, PyAny>>,
pub declared_vars: Option<Bound<'py, PyAny>>,
}

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

#[pyfunction]
fn circuit_to_dag(
py: Python,
quantum_circuit: QuantumCircuitData,
qubit_order: Option<Vec<PyObject>>,
clbit_order: Option<Vec<PyObject>>,
) -> PyResult<DAGCircuit> {
DAGCircuit::from_quantum_circuit(py, quantum_circuit, qubit_order, clbit_order)
}

pub fn converters(m: &Bound<PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(circuit_to_dag, m)?)?;
Ok(())
}
233 changes: 233 additions & 0 deletions crates/circuit/src/dag_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::bit_data::BitData;
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 @@ -6376,6 +6377,238 @@ impl DAGCircuit {

Ok(new_nodes)
}

/// Alternative constructor to build an instance of [DAGCircuit] from a `QuantumCircuit`.
pub(crate) fn from_quantum_circuit(
py: Python,
qc: QuantumCircuitData,
qubit_order: Option<Vec<PyObject>>,
clbit_order: Option<Vec<PyObject>>,
) -> 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 mut num_vars = 0;
if let Some(vars) = &qc.input_vars {
num_vars += vars.len()?;
}
if let Some(vars) = &qc.captured_vars {
num_vars += vars.len()?;
}
if let Some(vars) = &qc.declared_vars {
num_vars += vars.len()?;
}
let mut num_edges = 0;

// Take ownership of the interners.
let mut qubit_interner = if qubit_order.is_some() {
IndexedInterner::new()
} else {
qc_data.qargs_interner().clone()
};

let mut clbit_interner = if clbit_order.is_some() {
IndexedInterner::new()
} else {
qc_data.cargs_interner().clone()
};

// Take ownership of the bit_data
let mut qubit_data = if qubit_order.is_some() {
BitData::with_capacity(py, "qubits".to_string(), num_qubits)
} else {
qc_data.qubits().clone()
};

let mut clbit_data = if clbit_order.is_some() {
BitData::with_capacity(py, "clbits".to_string(), num_clbits)
} else {
qc_data.clbits().clone()
};

// Pre-process the instructions
let qubit_set: Vec<Qubit> = if let Some(qubit_ordering) = &qubit_order {
if qubit_ordering.len() != num_qubits {
return Err(PyValueError::new_err(
"'qubit_order' does not contain exactly the same qubits as the circuit",
));
};
let mut qubits = vec![];
for qubit in qubit_ordering {
let bound = qubit.bind(py);
if qubit_data.find(bound).is_some() {
return Err(DAGCircuitError::new_err(format!(
"duplicate qubits {}",
bound.repr()?
)));
}
// Add bit to its respective BitData and add index to qubit_set
qubit_data.add(py, bound, false)?;
if let Some(qubit) = qc_data.qubits().find(bound) {
qubits.push(qubit);
}
}
qubits
} else {
(0..num_qubits as u32).map(Qubit).collect()
};
let clbit_set: Vec<Clbit> = if let Some(clbit_ordering) = &clbit_order {
if clbit_ordering.len() != num_clbits {
return Err(PyValueError::new_err(
"'clbit_order' does not contain exactly the same clbits as the circuit",
));
};
let mut clbits = vec![];
for clbit in clbit_ordering {
let bound = clbit.bind(py);
if clbit_data.find(bound).is_some() {
return Err(DAGCircuitError::new_err(format!(
"duplicate clbits {}",
bound.repr()?
)));
}
// Add bit to its respective BitData and add index to clbit_set
clbit_data.add(py, bound, false)?;
if let Some(clbit) = qc_data.clbits().find(bound) {
clbits.push(clbit);
}
}
clbits
} else {
(0..num_clbits as u32).map(Clbit).collect()
};

// Count all input nodes in each instruction
let instructions: Vec<PackedInstruction> = qc_data
.iter()
.cloned()
.map(|instr| -> PyResult<PackedInstruction> {
// Re-map the qubits
let qargs: Vec<Qubit> = qc_data.get_qargs(instr.qubits).to_vec();
if qubit_order.is_some() {
let ordered_qargs = qargs
.iter()
.map(|index| qubit_set[index.0 as usize])
.collect();
Interner::intern(&mut qubit_interner, ordered_qargs)?;
}
// Remap the clbits
let cargs: Vec<Clbit> = qc_data.get_cargs(instr.clbits).to_vec();
if clbit_order.is_some() {
let ordered_cargs = cargs
.iter()
.map(|index| clbit_set[index.0 as usize])
.collect();
Interner::intern(&mut clbit_interner, ordered_cargs)?;
}

num_edges += qargs.len() + cargs.len();

Ok(instr)
})
.collect::<PyResult<Vec<_>>>()?;

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

// 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"),
};

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

// TODO: Use Qubit ordering to remap the interners and qubit

// Copy over all interners and registers
new_dag.qargs_cache = qubit_interner;
new_dag.cargs_cache = clbit_interner;

new_dag.qubits = qubit_data;
new_dag.clbits = clbit_data;

// Re-map and add all of the qubits
for qubit in (0..num_qubits as u32).map(Qubit) {
if let Some(bit) = new_dag.qubits.get(qubit) {
new_dag.qubit_locations.bind(py).set_item(
bit,
BitLocations {
index: qubit.0 as usize,
registers: PyList::empty_bound(py).unbind(),
}
.into_py(py),
)?;
new_dag.add_wire(py, Wire::Qubit(qubit))?;
}
}

// Re-map and add all of the qubits
for clbit in (0..num_clbits as u32).map(Clbit) {
if let Some(bit) = new_dag.clbits.get(clbit) {
new_dag.clbit_locations.bind(py).set_item(
bit,
BitLocations {
index: clbit.0 as usize,
registers: PyList::empty_bound(py).unbind(),
}
.into_py(py),
)?;
new_dag.add_wire(py, Wire::Clbit(clbit))?;
}
}

if let Some(vars) = qc.declared_vars {
for var in vars.iter()? {
new_dag.add_var(py, &var?, DAGVarType::Declare)?;
}
}

if let Some(vars) = qc.input_vars {
for var in vars.iter()? {
new_dag.add_var(py, &var?, DAGVarType::Input)?;
}
}

if let Some(vars) = qc.captured_vars {
for var in vars.iter()? {
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)?;
}
}

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

Ok(new_dag)
}
}

/// Add to global phase. Global phase can only be Float or ParameterExpression so this
Expand Down
13 changes: 13 additions & 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 Expand Up @@ -80,6 +81,17 @@ impl From<Clbit> for BitType {
}
}

#[inline(always)]
#[doc(hidden)]
fn add_submodule<F>(m: &Bound<PyModule>, constructor: F, name: &str) -> PyResult<()>
where
F: FnOnce(&Bound<PyModule>) -> PyResult<()>,
{
let new_mod = PyModule::new_bound(m.py(), name)?;
constructor(&new_mod)?;
m.add_submodule(&new_mod)
}

pub fn circuit(m: &Bound<PyModule>) -> PyResult<()> {
m.add_class::<circuit_data::CircuitData>()?;
m.add_class::<circuit_instruction::CircuitInstruction>()?;
Expand All @@ -89,5 +101,6 @@ pub fn circuit(m: &Bound<PyModule>) -> PyResult<()> {
m.add_class::<dag_node::DAGOutNode>()?;
m.add_class::<dag_node::DAGOpNode>()?;
m.add_class::<operations::StandardGate>()?;
add_submodule(m, converters::converters, "converters")?;
Ok(())
}
1 change: 1 addition & 0 deletions qiskit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
# We manually define them on import so people can directly import qiskit._accelerate.* submodules
# 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.converters"] = _accelerate.circuit.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 7aa6d71

Please sign in to comment.