diff --git a/crates/circuit/src/converters.rs b/crates/circuit/src/converters.rs new file mode 100644 index 000000000000..ff061ab9cf48 --- /dev/null +++ b/crates/circuit/src/converters.rs @@ -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>, + pub calibrations: HashMap>, + pub metadata: Option>, + pub qregs: Option>, + pub cregs: Option>, + pub input_vars: Option>, + pub captured_vars: Option>, + pub declared_vars: Option>, +} + +impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + let circuit_data = ob.getattr("_data")?; + let data_borrowed = circuit_data.downcast::()?.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>, + clbit_order: Option>, +) -> PyResult { + DAGCircuit::from_quantum_circuit(py, quantum_circuit, qubit_order, clbit_order) +} + +pub fn converters(m: &Bound) -> PyResult<()> { + m.add_function(wrap_pyfunction!(circuit_to_dag, m)?)?; + Ok(()) +} diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 3ed5da718183..335e08d0d38d 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -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; @@ -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>, + clbit_order: Option>, + ) -> PyResult { + // 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 = 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 = 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 = qc_data + .iter() + .cloned() + .map(|instr| -> PyResult { + // Re-map the qubits + let qargs: Vec = 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 = 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::>>()?; + + // 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 diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 4ca86c2ca83c..e01bd9a916c8 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -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; @@ -80,6 +81,17 @@ impl From for BitType { } } +#[inline(always)] +#[doc(hidden)] +fn add_submodule(m: &Bound, constructor: F, name: &str) -> PyResult<()> +where + F: FnOnce(&Bound) -> PyResult<()>, +{ + let new_mod = PyModule::new_bound(m.py(), name)?; + constructor(&new_mod)?; + m.add_submodule(&new_mod) +} + pub fn circuit(m: &Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; @@ -89,5 +101,6 @@ pub fn circuit(m: &Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + add_submodule(m, converters::converters, "converters")?; Ok(()) } diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 6a8df393307e..36490b2daa73 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -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 diff --git a/qiskit/converters/circuit_to_dag.py b/qiskit/converters/circuit_to_dag.py index a330b8cbd682..f9d1cb3d7fca 100644 --- a/qiskit/converters/circuit_to_dag.py +++ b/qiskit/converters/circuit_to_dag.py @@ -14,6 +14,7 @@ from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.dagcircuit.dagnode import DAGOpNode +from qiskit._accelerate.circuit.converters import circuit_to_dag as core_circuit_to_dag def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_order=None): @@ -56,46 +57,7 @@ def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_ord circ.rz(0.5, q[1]).c_if(c, 2) dag = circuit_to_dag(circ) """ - dagcircuit = DAGCircuit() - dagcircuit.name = circuit.name - dagcircuit.global_phase = circuit.global_phase - dagcircuit.calibrations = circuit.calibrations - dagcircuit.metadata = circuit.metadata - - if qubit_order is None: - qubits = circuit.qubits - elif len(qubit_order) != circuit.num_qubits or set(qubit_order) != set(circuit.qubits): - raise ValueError("'qubit_order' does not contain exactly the same qubits as the circuit") - else: - qubits = qubit_order - - if clbit_order is None: - clbits = circuit.clbits - elif len(clbit_order) != circuit.num_clbits or set(clbit_order) != set(circuit.clbits): - raise ValueError("'clbit_order' does not contain exactly the same clbits as the circuit") - else: - clbits = clbit_order - - dagcircuit.add_qubits(qubits) - dagcircuit.add_clbits(clbits) - - for var in circuit.iter_input_vars(): - dagcircuit.add_input_var(var) - for var in circuit.iter_captured_vars(): - dagcircuit.add_captured_var(var) - for var in circuit.iter_declared_vars(): - dagcircuit.add_declared_var(var) - - for register in circuit.qregs: - dagcircuit.add_qreg(register) - - for register in circuit.cregs: - dagcircuit.add_creg(register) - - for instruction in circuit.data: - dagcircuit._apply_op_node_back( - DAGOpNode.from_instruction(instruction, deepcopy=copy_operations) - ) + dagcircuit = core_circuit_to_dag(circuit, qubit_order, clbit_order) dagcircuit.duration = circuit.duration dagcircuit.unit = circuit.unit