diff --git a/crates/circuit/src/converters.rs b/crates/circuit/src/converters.rs new file mode 100644 index 000000000000..ab9d4bce0475 --- /dev/null +++ b/crates/circuit/src/converters.rs @@ -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>, + pub calibrations: Option>>, + pub metadata: Option>, + pub qregs: Option>, + pub cregs: Option>, + pub input_vars: Vec>, + pub captured_vars: Vec>, + pub declared_vars: Vec>, +} + +impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + let py = ob.py(); + let circuit_data = ob.getattr("_data")?; + let data_borrowed = circuit_data.extract::()?; + 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::>>()?, + captured_vars: ob + .call_method0(intern!(py, "iter_captured_vars"))? + .iter()? + .collect::>>()?, + declared_vars: ob + .call_method0(intern!(py, "iter_declared_vars"))? + .iter()? + .collect::>>()?, + }) + } +} + +#[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>>, + clbit_order: Option>>, +) -> PyResult { + DAGCircuit::from_circuit( + py, + quantum_circuit, + copy_operations, + 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 0b5a43c1eb4b..a04ab378e01e 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -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; @@ -6562,7 +6564,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))); } @@ -6590,6 +6597,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>>, + 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 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> = 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> = 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 = qc_data + .iter() + .map(|instr| -> PyResult { + // 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::>>()?; + + // 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 { + 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 diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 5106ba030288..dcff558ade64 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; diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 03bd0202dae4..72c40bba88ec 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -42,6 +42,7 @@ where #[pymodule] fn _accelerate(m: &Bound) -> 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")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 29e2b8ba5c11..894f9710d431 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -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 diff --git a/qiskit/converters/circuit_to_dag.py b/qiskit/converters/circuit_to_dag.py index a330b8cbd682..5be9a721bafa 100644 --- a/qiskit/converters/circuit_to_dag.py +++ b/qiskit/converters/circuit_to_dag.py @@ -12,8 +12,8 @@ """Helper function for converting a circuit to a dag""" -from qiskit.dagcircuit.dagcircuit import DAGCircuit -from qiskit.dagcircuit.dagnode import DAGOpNode +from qiskit.circuit.library.blueprintcircuit import BlueprintCircuit +from qiskit._accelerate.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 +56,22 @@ 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): + # If we have an instance of BluePrintCircuit, make sure it is built by calling ._build() + if isinstance(circuit, BlueprintCircuit): + if not circuit._is_built: + circuit._build() + + if qubit_order is not None and ( + 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): + if clbit_order is not None and ( + 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, copy_operations, qubit_order, clbit_order) dagcircuit.duration = circuit.duration dagcircuit.unit = circuit.unit