diff --git a/Cargo.lock b/Cargo.lock index 63e36f18645c..12daf2e6299e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1187,12 +1187,18 @@ dependencies = [ name = "qiskit-circuit" version = "1.3.0" dependencies = [ + "ahash 0.8.11", + "approx", "bytemuck", "hashbrown 0.14.5", + "indexmap", + "itertools 0.13.0", "ndarray", "num-complex", "numpy", "pyo3", + "rayon", + "rustworkx-core", "smallvec", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index f6141f787fdf..a35a87d4d3b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,14 +16,18 @@ license = "Apache-2.0" [workspace.dependencies] bytemuck = "1.17" indexmap.version = "2.4.0" -hashbrown.version = "0.14.0" +hashbrown.version = "0.14.5" num-bigint = "0.4" num-complex = "0.4" ndarray = "^0.15.6" numpy = "0.21.0" smallvec = "1.13" thiserror = "1.0" +rustworkx-core = "0.15" +approx = "0.5" +itertools = "0.13.0" ahash = "0.8.11" +rayon = "1.10" # Most of the crates don't need the feature `extension-module`, since only `qiskit-pyext` builds an # actual C extension (the feature disables linking in `libpython`, which is forbidden in Python diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index c93e81b14dee..838fc0153577 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -10,7 +10,7 @@ name = "qiskit_accelerate" doctest = false [dependencies] -rayon = "1.10" +rayon.workspace = true numpy.workspace = true rand = "0.8" rand_pcg = "0.3" @@ -18,10 +18,10 @@ rand_distr = "0.4.3" ahash.workspace = true num-traits = "0.2" num-complex.workspace = true +rustworkx-core.workspace = true num-bigint.workspace = true -rustworkx-core = "0.15" faer = "0.19.1" -itertools = "0.13.0" +itertools.workspace = true qiskit-circuit.workspace = true thiserror.workspace = true @@ -38,7 +38,7 @@ workspace = true features = ["rayon", "approx-0_5"] [dependencies.approx] -version = "0.5" +workspace = true features = ["num-complex"] [dependencies.hashbrown] diff --git a/crates/accelerate/src/convert_2q_block_matrix.rs b/crates/accelerate/src/convert_2q_block_matrix.rs index e9f6e343b6bd..dc4d0b77c4a7 100644 --- a/crates/accelerate/src/convert_2q_block_matrix.rs +++ b/crates/accelerate/src/convert_2q_block_matrix.rs @@ -27,7 +27,7 @@ use qiskit_circuit::circuit_instruction::CircuitInstruction; use qiskit_circuit::dag_node::DAGOpNode; use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY; use qiskit_circuit::imports::QI_OPERATOR; -use qiskit_circuit::operations::{Operation, OperationRef}; +use qiskit_circuit::operations::Operation; use crate::QiskitError; @@ -35,7 +35,7 @@ fn get_matrix_from_inst<'py>( py: Python<'py>, inst: &'py CircuitInstruction, ) -> PyResult> { - if let Some(mat) = inst.op().matrix(&inst.params) { + if let Some(mat) = inst.operation.matrix(&inst.params) { Ok(mat) } else if inst.operation.try_standard_gate().is_some() { Err(QiskitError::new_err( @@ -124,29 +124,7 @@ pub fn change_basis(matrix: ArrayView2) -> Array2 { trans_matrix } -#[pyfunction] -pub fn collect_2q_blocks_filter(node: &Bound) -> Option { - let Ok(node) = node.downcast::() else { - return None; - }; - let node = node.borrow(); - match node.instruction.op() { - gate @ (OperationRef::Standard(_) | OperationRef::Gate(_)) => Some( - gate.num_qubits() <= 2 - && node - .instruction - .extra_attrs - .as_ref() - .and_then(|attrs| attrs.condition.as_ref()) - .is_none() - && !node.is_parameterized(), - ), - _ => Some(false), - } -} - pub fn convert_2q_block_matrix(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(blocks_to_matrix))?; - m.add_wrapped(wrap_pyfunction!(collect_2q_blocks_filter))?; Ok(()) } diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 7463777af624..75a2f7993f86 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -743,7 +743,7 @@ pub fn compute_error_list( .iter() .map(|node| { ( - node.instruction.op().name().to_string(), + node.instruction.operation.name().to_string(), smallvec![], // Params not needed in this path ) }) @@ -988,10 +988,11 @@ pub fn optimize_1q_gates_decomposition( .iter() .map(|node| { if let Some(err_map) = error_map { - error *= compute_error_term(node.instruction.op().name(), err_map, qubit) + error *= + compute_error_term(node.instruction.operation.name(), err_map, qubit) } node.instruction - .op() + .operation .matrix(&node.instruction.params) .expect("No matrix defined for operation") }) @@ -1043,22 +1044,6 @@ fn matmul_1q(operator: &mut [[Complex64; 2]; 2], other: Array2) { ]; } -#[pyfunction] -pub fn collect_1q_runs_filter(node: &Bound) -> bool { - let Ok(node) = node.downcast::() else { - return false; - }; - let node = node.borrow(); - let op = node.instruction.op(); - op.num_qubits() == 1 - && op.num_clbits() == 0 - && op.matrix(&node.instruction.params).is_some() - && match &node.instruction.extra_attrs { - None => true, - Some(attrs) => attrs.condition.is_none(), - } -} - pub fn euler_one_qubit_decomposer(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(params_zyz))?; m.add_wrapped(wrap_pyfunction!(params_xyx))?; @@ -1072,7 +1057,6 @@ pub fn euler_one_qubit_decomposer(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(compute_error_one_qubit_sequence))?; m.add_wrapped(wrap_pyfunction!(compute_error_list))?; m.add_wrapped(wrap_pyfunction!(optimize_1q_gates_decomposition))?; - m.add_wrapped(wrap_pyfunction!(collect_1q_runs_filter))?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 3eb430515fcf..ed1f849bbf62 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -10,17 +10,29 @@ name = "qiskit_circuit" doctest = false [dependencies] +rayon.workspace = true +ahash.workspace = true +rustworkx-core.workspace = true bytemuck.workspace = true -hashbrown.workspace = true num-complex.workspace = true ndarray.workspace = true numpy.workspace = true thiserror.workspace = true +approx.workspace = true +itertools.workspace = true [dependencies.pyo3] workspace = true features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"] +[dependencies.hashbrown] +workspace = true +features = ["rayon"] + +[dependencies.indexmap] +workspace = true +features = ["rayon"] + [dependencies.smallvec] workspace = true features = ["union"] diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 977d1b34e496..0c0b20a02522 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -81,17 +81,6 @@ pub struct BitData { cached: Py, } -pub struct BitNotFoundError<'py>(pub(crate) Bound<'py, PyAny>); - -impl<'py> From> for PyErr { - fn from(error: BitNotFoundError) -> Self { - PyKeyError::new_err(format!( - "Bit {:?} has not been added to this circuit.", - error.0 - )) - } -} - impl BitData where T: From + Copy, @@ -139,14 +128,19 @@ where pub fn map_bits<'py>( &self, bits: impl IntoIterator>, - ) -> Result, BitNotFoundError<'py>> { + ) -> PyResult> { let v: Result, _> = bits .into_iter() .map(|b| { self.indices .get(&BitAsKey::new(&b)) .copied() - .ok_or_else(|| BitNotFoundError(b)) + .ok_or_else(|| { + PyKeyError::new_err(format!( + "Bit {:?} has not been added to this circuit.", + b + )) + }) }) .collect(); v.map(|x| x.into_iter()) @@ -168,7 +162,7 @@ where } /// Adds a new Python bit. - pub fn add(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { + pub fn add(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult { if self.bits.len() != self.cached.bind(bit.py()).len() { return Err(PyRuntimeError::new_err( format!("This circuit's {} list has become out of sync with the circuit data. Did something modify it?", self.description) @@ -193,6 +187,29 @@ where bit ))); } + Ok(idx.into()) + } + + pub fn remove_indices(&mut self, py: Python, indices: I) -> PyResult<()> + where + I: IntoIterator, + { + let mut indices_sorted: Vec = indices + .into_iter() + .map(|i| >::from(i) as usize) + .collect(); + indices_sorted.sort(); + + for index in indices_sorted.into_iter().rev() { + self.cached.bind(py).del_item(index)?; + let bit = self.bits.remove(index); + self.indices.remove(&BitAsKey::new(bit.bind(py))); + } + // Update indices. + for (i, bit) in self.bits.iter().enumerate() { + self.indices + .insert(BitAsKey::new(bit.bind(py)), (i as BitType).into()); + } Ok(()) } diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index d29455c5363b..4dd3956bee6a 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -16,7 +16,7 @@ use std::cell::OnceCell; use crate::bit_data::BitData; use crate::circuit_instruction::{CircuitInstruction, OperationFromPython}; use crate::imports::{ANNOTATED_OPERATION, CLBIT, QUANTUM_CIRCUIT, QUBIT}; -use crate::interner::{IndexedInterner, Interner, InternerKey}; +use crate::interner::{IndexedInterner, Interner}; use crate::operations::{Operation, OperationRef, Param, StandardGate}; use crate::packed_instruction::{PackedInstruction, PackedOperation}; use crate::parameter_table::{ParameterTable, ParameterTableError, ParameterUse, ParameterUuid}; @@ -148,12 +148,8 @@ impl CircuitData { global_phase, )?; for (operation, params, qargs, cargs) in instruction_iter { - let qubits = (&mut res.qargs_interner) - .intern(InternerKey::Value(qargs))? - .index; - let clbits = (&mut res.cargs_interner) - .intern(InternerKey::Value(cargs))? - .index; + let qubits = (&mut res.qargs_interner).intern(qargs)?; + let clbits = (&mut res.cargs_interner).intern(cargs)?; let params = (!params.is_empty()).then(|| Box::new(params)); res.data.push(PackedInstruction { op: operation, @@ -203,13 +199,9 @@ impl CircuitData { instruction_iter.size_hint().0, global_phase, )?; - let no_clbit_index = (&mut res.cargs_interner) - .intern(InternerKey::Value(Vec::new()))? - .index; + let no_clbit_index = (&mut res.cargs_interner).intern(Vec::new())?; for (operation, params, qargs) in instruction_iter { - let qubits = (&mut res.qargs_interner) - .intern(InternerKey::Value(qargs.to_vec()))? - .index; + let qubits = (&mut res.qargs_interner).intern(qargs.to_vec())?; let params = (!params.is_empty()).then(|| Box::new(params)); res.data.push(PackedInstruction { op: operation.into(), @@ -266,13 +258,9 @@ impl CircuitData { params: &[Param], qargs: &[Qubit], ) -> PyResult<()> { - let no_clbit_index = (&mut self.cargs_interner) - .intern(InternerKey::Value(Vec::new()))? - .index; + let no_clbit_index = (&mut self.cargs_interner).intern(Vec::new())?; let params = (!params.is_empty()).then(|| Box::new(params.iter().cloned().collect())); - let qubits = (&mut self.qargs_interner) - .intern(InternerKey::Value(qargs.to_vec()))? - .index; + let qubits = (&mut self.qargs_interner).intern(qargs.to_vec())?; self.data.push(PackedInstruction { op: operation.into(), qubits, @@ -497,7 +485,8 @@ impl CircuitData { /// was provided. #[pyo3(signature = (bit, *, strict=true))] pub fn add_qubit(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { - self.qubits.add(py, bit, strict) + self.qubits.add(py, bit, strict)?; + Ok(()) } /// Registers a :class:`.Clbit` instance. @@ -511,7 +500,8 @@ impl CircuitData { /// was provided. #[pyo3(signature = (bit, *, strict=true))] pub fn add_clbit(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { - self.clbits.add(py, bit, strict) + self.clbits.add(py, bit, strict)?; + Ok(()) } /// Performs a shallow copy. @@ -582,10 +572,10 @@ impl CircuitData { let qubits = PySet::empty_bound(py)?; let clbits = PySet::empty_bound(py)?; for inst in self.data.iter() { - for b in self.qargs_interner.intern(inst.qubits).value.iter() { + for b in self.qargs_interner.intern(inst.qubits) { qubits.add(self.qubits.get(*b).unwrap().clone_ref(py))?; } - for b in self.cargs_interner.intern(inst.clbits).value.iter() { + for b in self.cargs_interner.intern(inst.clbits) { clbits.add(self.clbits.get(*b).unwrap().clone_ref(py))?; } } @@ -751,8 +741,8 @@ impl CircuitData { let clbits = self.cargs_interner.intern(inst.clbits); CircuitInstruction { operation: inst.op.clone(), - qubits: PyTuple::new_bound(py, self.qubits.map_indices(qubits.value)).unbind(), - clbits: PyTuple::new_bound(py, self.clbits.map_indices(clbits.value)).unbind(), + qubits: PyTuple::new_bound(py, self.qubits.map_indices(qubits)).unbind(), + clbits: PyTuple::new_bound(py, self.clbits.map_indices(clbits)).unbind(), params: inst.params_view().iter().cloned().collect(), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] @@ -905,7 +895,6 @@ impl CircuitData { let qubits = other .qargs_interner .intern(inst.qubits) - .value .iter() .map(|b| { Ok(self @@ -917,7 +906,6 @@ impl CircuitData { let clbits = other .cargs_interner .intern(inst.clbits) - .value .iter() .map(|b| { Ok(self @@ -927,14 +915,12 @@ impl CircuitData { }) .collect::>>()?; let new_index = self.data.len(); - let qubits_id = - Interner::intern(&mut self.qargs_interner, InternerKey::Value(qubits))?; - let clbits_id = - Interner::intern(&mut self.cargs_interner, InternerKey::Value(clbits))?; + let qubits_id = Interner::intern(&mut self.qargs_interner, qubits)?; + let clbits_id = Interner::intern(&mut self.cargs_interner, clbits)?; self.data.push(PackedInstruction { op: inst.op.clone(), - qubits: qubits_id.index, - clbits: clbits_id.index, + qubits: qubits_id, + clbits: clbits_id, params: inst.params.clone(), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] @@ -1106,7 +1092,7 @@ impl CircuitData { pub fn num_nonlocal_gates(&self) -> usize { self.data .iter() - .filter(|inst| inst.op().num_qubits() > 1 && !inst.op().directive()) + .filter(|inst| inst.op.num_qubits() > 1 && !inst.op.directive()) .count() } } @@ -1129,16 +1115,16 @@ impl CircuitData { fn pack(&mut self, py: Python, inst: &CircuitInstruction) -> PyResult { let qubits = Interner::intern( &mut self.qargs_interner, - InternerKey::Value(self.qubits.map_bits(inst.qubits.bind(py))?.collect()), + self.qubits.map_bits(inst.qubits.bind(py))?.collect(), )?; let clbits = Interner::intern( &mut self.cargs_interner, - InternerKey::Value(self.clbits.map_bits(inst.clbits.bind(py))?.collect()), + self.clbits.map_bits(inst.clbits.bind(py))?.collect(), )?; Ok(PackedInstruction { op: inst.operation.clone(), - qubits: qubits.index, - clbits: clbits.index, + qubits, + clbits, params: (!inst.params.is_empty()).then(|| Box::new(inst.params.clone())), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index ae649b5a8110..b0620d78fb75 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -114,11 +114,6 @@ pub struct CircuitInstruction { } impl CircuitInstruction { - /// View the operation in this `CircuitInstruction`. - pub fn op(&self) -> OperationRef { - self.operation.view() - } - /// Get the Python-space operation, ensuring that it is mutable from Python space (singleton /// gates might not necessarily satisfy this otherwise). /// @@ -135,6 +130,12 @@ impl CircuitInstruction { out.call_method0(intern!(py, "to_mutable")) } } + + pub fn condition(&self) -> Option<&PyObject> { + self.extra_attrs + .as_ref() + .and_then(|args| args.condition.as_ref()) + } } #[pymethods] @@ -230,7 +231,7 @@ impl CircuitInstruction { /// Returns the Instruction name corresponding to the op for this node #[getter] fn get_name(&self, py: Python) -> PyObject { - self.op().name().to_object(py) + self.operation.name().to_object(py) } #[getter] @@ -252,7 +253,7 @@ impl CircuitInstruction { } #[getter] - fn condition(&self, py: Python) -> Option { + fn get_condition(&self, py: Python) -> Option { self.extra_attrs .as_ref() .and_then(|attrs| attrs.condition.as_ref().map(|x| x.clone_ref(py))) @@ -292,13 +293,13 @@ impl CircuitInstruction { /// Is the :class:`.Operation` contained in this node a directive? pub fn is_directive(&self) -> bool { - self.op().directive() + self.operation.directive() } /// Is the :class:`.Operation` contained in this instruction a control-flow operation (i.e. an /// instance of :class:`.ControlFlowOp`)? pub fn is_control_flow(&self) -> bool { - self.op().control_flow() + self.operation.control_flow() } /// Does this instruction contain any :class:`.ParameterExpression` parameters? diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs new file mode 100644 index 000000000000..bb5fff5e343a --- /dev/null +++ b/crates/circuit/src/dag_circuit.rs @@ -0,0 +1,6196 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 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 std::hash::{Hash, Hasher}; + +use ahash::RandomState; + +use crate::bit_data::BitData; +use crate::circuit_instruction::{ + CircuitInstruction, ExtraInstructionAttributes, OperationFromPython, +}; +use crate::dag_node::{DAGInNode, DAGNode, DAGOpNode, DAGOutNode}; +use crate::dot_utils::build_dot; +use crate::error::DAGCircuitError; +use crate::imports; +use crate::interner::{IndexedInterner, Interner}; +use crate::operations::{Operation, OperationRef, Param, PyInstruction}; +use crate::packed_instruction::PackedInstruction; +use crate::rustworkx_core_vnext::isomorphism; +use crate::{BitType, Clbit, Qubit, TupleLikeArg}; + +use hashbrown::{HashMap, HashSet}; +use indexmap::IndexMap; +use itertools::Itertools; + +use pyo3::exceptions::{PyIndexError, PyRuntimeError, PyTypeError, PyValueError}; +use pyo3::intern; +use pyo3::prelude::*; +use pyo3::types::{ + IntoPyDict, PyDict, PyInt, PyIterator, PyList, PySequence, PySet, PyString, PyTuple, PyType, +}; + +use rustworkx_core::dag_algo::layers; +use rustworkx_core::err::ContractError; +use rustworkx_core::graph_ext::ContractNodesDirected; +use rustworkx_core::petgraph; +use rustworkx_core::petgraph::prelude::StableDiGraph; +use rustworkx_core::petgraph::prelude::*; +use rustworkx_core::petgraph::stable_graph::{EdgeReference, NodeIndex}; +use rustworkx_core::petgraph::unionfind::UnionFind; +use rustworkx_core::petgraph::visit::{ + EdgeIndexable, IntoEdgeReferences, IntoNodeReferences, NodeFiltered, NodeIndexable, +}; +use rustworkx_core::petgraph::Incoming; +use rustworkx_core::traversal::{ + ancestors as core_ancestors, bfs_successors as core_bfs_successors, + descendants as core_descendants, +}; + +use std::cmp::Ordering; +use std::collections::{BTreeMap, VecDeque}; +use std::convert::Infallible; +use std::f64::consts::PI; + +#[cfg(feature = "cache_pygates")] +use std::cell::OnceCell; + +static CONTROL_FLOW_OP_NAMES: [&str; 4] = ["for_loop", "while_loop", "if_else", "switch_case"]; +static SEMANTIC_EQ_SYMMETRIC: [&str; 4] = ["barrier", "swap", "break_loop", "continue_loop"]; + +#[derive(Clone, Debug)] +pub enum NodeType { + QubitIn(Qubit), + QubitOut(Qubit), + ClbitIn(Clbit), + ClbitOut(Clbit), + VarIn(PyObject), + VarOut(PyObject), + Operation(PackedInstruction), +} + +#[derive(Clone, Debug)] +pub enum Wire { + Qubit(Qubit), + Clbit(Clbit), + Var(PyObject), +} + +impl PartialEq for Wire { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Wire::Qubit(q1), Wire::Qubit(q2)) => q1 == q2, + (Wire::Clbit(c1), Wire::Clbit(c2)) => c1 == c2, + (Wire::Var(v1), Wire::Var(v2)) => { + v1.is(v2) || Python::with_gil(|py| v1.bind(py).eq(v2).unwrap()) + } + _ => false, + } + } +} + +impl Eq for Wire {} + +impl Hash for Wire { + fn hash(&self, state: &mut H) { + match self { + Self::Qubit(qubit) => qubit.hash(state), + Self::Clbit(clbit) => clbit.hash(state), + Self::Var(var) => Python::with_gil(|py| var.bind(py).hash().unwrap().hash(state)), + } + } +} + +impl Wire { + fn to_pickle(&self, py: Python) -> PyObject { + match self { + Self::Qubit(bit) => (0, bit.0.into_py(py)).into_py(py), + Self::Clbit(bit) => (1, bit.0.into_py(py)).into_py(py), + Self::Var(var) => (2, var.clone_ref(py)).into_py(py), + } + } + + fn from_pickle(b: &Bound) -> PyResult { + let tuple: Bound = b.extract()?; + let wire_type: usize = tuple.get_item(0)?.extract()?; + if wire_type == 0 { + Ok(Self::Qubit(Qubit(tuple.get_item(1)?.extract()?))) + } else if wire_type == 1 { + Ok(Self::Clbit(Clbit(tuple.get_item(1)?.extract()?))) + } else if wire_type == 2 { + Ok(Self::Var(tuple.get_item(1)?.unbind())) + } else { + Err(PyTypeError::new_err("Invalid wire type")) + } + } +} + +// TODO: Remove me. +// This is a temporary map type used to store a mapping of +// Var to NodeIndex to hold us over until Var is ported to +// Rust. Currently, we need this because PyObject cannot be +// used as the key to an IndexMap. +// +// Once we've got Var ported, Wire should also become Hash + Eq +// and we can consider combining input/output nodes maps. +#[derive(Clone, Debug)] +struct _VarIndexMap { + dict: Py, +} + +impl _VarIndexMap { + pub fn new(py: Python) -> Self { + Self { + dict: PyDict::new_bound(py).unbind(), + } + } + + pub fn keys(&self, py: Python) -> impl Iterator { + self.dict + .bind(py) + .keys() + .into_iter() + .map(|k| k.unbind()) + .collect::>() + .into_iter() + } + + pub fn contains_key(&self, py: Python, key: &PyObject) -> bool { + self.dict.bind(py).contains(key).unwrap() + } + + pub fn get(&self, py: Python, key: &PyObject) -> Option { + self.dict + .bind(py) + .get_item(key) + .unwrap() + .map(|v| NodeIndex::new(v.extract().unwrap())) + } + + pub fn insert(&mut self, py: Python, key: PyObject, value: NodeIndex) { + self.dict + .bind(py) + .set_item(key, value.index().into_py(py)) + .unwrap() + } + + pub fn remove(&mut self, py: Python, key: &PyObject) -> Option { + let bound_dict = self.dict.bind(py); + let res = bound_dict + .get_item(key.clone_ref(py)) + .unwrap() + .map(|v| NodeIndex::new(v.extract().unwrap())); + let _del_result = bound_dict.del_item(key); + res + } + pub fn values<'py>(&self, py: Python<'py>) -> impl Iterator + 'py { + let values = self.dict.bind(py).values(); + values.iter().map(|x| NodeIndex::new(x.extract().unwrap())) + } + + pub fn iter<'py>(&self, py: Python<'py>) -> impl Iterator + 'py { + self.dict + .bind(py) + .iter() + .map(|(var, index)| (var.unbind(), NodeIndex::new(index.extract().unwrap()))) + } +} + +/// Quantum circuit as a directed acyclic graph. +/// +/// There are 3 types of nodes in the graph: inputs, outputs, and operations. +/// The nodes are connected by directed edges that correspond to qubits and +/// bits. +#[pyclass(module = "qiskit._accelerate.circuit")] +#[derive(Clone, Debug)] +pub struct DAGCircuit { + /// Circuit name. Generally, this corresponds to the name + /// of the QuantumCircuit from which the DAG was generated. + #[pyo3(get, set)] + name: Option, + /// Circuit metadata + #[pyo3(get, set)] + metadata: Option, + + calibrations: HashMap>, + + pub dag: StableDiGraph, + + #[pyo3(get)] + qregs: Py, + #[pyo3(get)] + cregs: Py, + + /// The cache used to intern instruction qargs. + qargs_cache: IndexedInterner>, + /// The cache used to intern instruction cargs. + cargs_cache: IndexedInterner>, + /// Qubits registered in the circuit. + pub qubits: BitData, + /// Clbits registered in the circuit. + pub clbits: BitData, + /// Global phase. + global_phase: Param, + /// Duration. + #[pyo3(get, set)] + duration: Option, + /// Unit of duration. + #[pyo3(get, set)] + unit: String, + + // Note: these are tracked separately from `qubits` and `clbits` + // because it's not yet clear if the Rust concept of a native Qubit + // and Clbit should correspond directly to the numerical Python + // index that users see in the Python API. + /// The index locations of bits, and their positions within + /// registers. + qubit_locations: Py, + clbit_locations: Py, + + /// Map from qubit to input and output nodes of the graph. + qubit_io_map: Vec<[NodeIndex; 2]>, + + /// Map from clbit to input and output nodes of the graph. + clbit_io_map: Vec<[NodeIndex; 2]>, + + // TODO: use IndexMap once Var is ported to Rust + /// Map from var to input nodes of the graph. + var_input_map: _VarIndexMap, + /// Map from var to output nodes of the graph. + var_output_map: _VarIndexMap, + + /// Operation kind to count + op_names: IndexMap, + + // Python modules we need to frequently access (for now). + control_flow_module: PyControlFlowModule, + vars_info: HashMap, + vars_by_type: [Py; 3], +} + +#[derive(Clone, Debug)] +struct PyControlFlowModule { + condition_resources: Py, + node_resources: Py, +} + +#[derive(Clone, Debug)] +struct PyLegacyResources { + clbits: Py, + cregs: Py, +} + +impl PyControlFlowModule { + fn new(py: Python) -> PyResult { + let module = PyModule::import_bound(py, "qiskit.circuit.controlflow")?; + Ok(PyControlFlowModule { + condition_resources: module.getattr("condition_resources")?.unbind(), + node_resources: module.getattr("node_resources")?.unbind(), + }) + } + + fn condition_resources(&self, condition: &Bound) -> PyResult { + let res = self + .condition_resources + .bind(condition.py()) + .call1((condition,))?; + Ok(PyLegacyResources { + clbits: res.getattr("clbits")?.downcast_into_exact()?.unbind(), + cregs: res.getattr("cregs")?.downcast_into_exact()?.unbind(), + }) + } + + fn node_resources(&self, node: &Bound) -> PyResult { + let res = self.node_resources.bind(node.py()).call1((node,))?; + Ok(PyLegacyResources { + clbits: res.getattr("clbits")?.downcast_into_exact()?.unbind(), + cregs: res.getattr("cregs")?.downcast_into_exact()?.unbind(), + }) + } +} + +struct PyVariableMapper { + mapper: Py, +} + +impl PyVariableMapper { + fn new( + py: Python, + target_cregs: Bound, + bit_map: Option>, + var_map: Option>, + add_register: Option>, + ) -> PyResult { + let kwargs: HashMap<&str, Option>> = + HashMap::from_iter([("add_register", add_register)]); + Ok(PyVariableMapper { + mapper: imports::VARIABLE_MAPPER + .get_bound(py) + .call( + (target_cregs, bit_map, var_map), + Some(&kwargs.into_py_dict_bound(py)), + )? + .unbind(), + }) + } + + fn map_condition<'py>( + &self, + condition: &Bound<'py, PyAny>, + allow_reorder: bool, + ) -> PyResult> { + let py = condition.py(); + let kwargs: HashMap<&str, Py> = + HashMap::from_iter([("allow_reorder", allow_reorder.into_py(py))]); + self.mapper.bind(py).call_method( + intern!(py, "map_condition"), + (condition,), + Some(&kwargs.into_py_dict_bound(py)), + ) + } + + fn map_target<'py>(&self, target: &Bound<'py, PyAny>) -> PyResult> { + let py = target.py(); + self.mapper + .bind(py) + .call_method1(intern!(py, "map_target"), (target,)) + } +} + +impl IntoPy> for PyVariableMapper { + fn into_py(self, _py: Python<'_>) -> Py { + self.mapper + } +} + +#[pyfunction] +fn reject_new_register(reg: &Bound) -> PyResult<()> { + Err(DAGCircuitError::new_err(format!( + "No register with '{:?}' to map this expression onto.", + reg.getattr("bits")? + ))) +} + +#[pyclass(module = "qiskit._accelerate.circuit")] +#[derive(Clone, Debug)] +struct BitLocations { + #[pyo3(get)] + index: usize, + #[pyo3(get)] + registers: Py, +} + +#[derive(Copy, Clone, Debug)] +enum DAGVarType { + Input = 0, + Capture = 1, + Declare = 2, +} + +#[derive(Clone, Debug)] +struct DAGVarInfo { + var: PyObject, + type_: DAGVarType, + in_node: NodeIndex, + out_node: NodeIndex, +} + +#[pymethods] +impl DAGCircuit { + #[new] + pub fn new(py: Python<'_>) -> PyResult { + Ok(DAGCircuit { + name: None, + metadata: Some(PyDict::new_bound(py).unbind().into()), + calibrations: HashMap::new(), + dag: StableDiGraph::default(), + qregs: PyDict::new_bound(py).unbind(), + cregs: PyDict::new_bound(py).unbind(), + qargs_cache: IndexedInterner::new(), + cargs_cache: IndexedInterner::new(), + qubits: BitData::new(py, "qubits".to_string()), + clbits: BitData::new(py, "clbits".to_string()), + global_phase: Param::Float(0.), + duration: None, + unit: "dt".to_string(), + qubit_locations: PyDict::new_bound(py).unbind(), + clbit_locations: PyDict::new_bound(py).unbind(), + qubit_io_map: Vec::new(), + clbit_io_map: Vec::new(), + var_input_map: _VarIndexMap::new(py), + var_output_map: _VarIndexMap::new(py), + op_names: IndexMap::default(), + control_flow_module: PyControlFlowModule::new(py)?, + vars_info: HashMap::new(), + vars_by_type: [ + PySet::empty_bound(py)?.unbind(), + PySet::empty_bound(py)?.unbind(), + PySet::empty_bound(py)?.unbind(), + ], + }) + } + + #[getter] + fn input_map(&self, py: Python) -> PyResult> { + let out_dict = PyDict::new_bound(py); + for (qubit, indices) in self + .qubit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Qubit(idx as u32), indices)) + { + out_dict.set_item( + self.qubits.get(qubit).unwrap().clone_ref(py), + self.get_node(py, indices[0])?, + )?; + } + for (clbit, indices) in self + .clbit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Clbit(idx as u32), indices)) + { + out_dict.set_item( + self.clbits.get(clbit).unwrap().clone_ref(py), + self.get_node(py, indices[0])?, + )?; + } + for (var, index) in self.var_input_map.dict.bind(py).iter() { + out_dict.set_item( + var, + self.get_node(py, NodeIndex::new(index.extract::()?))?, + )?; + } + Ok(out_dict.unbind()) + } + + #[getter] + fn output_map(&self, py: Python) -> PyResult> { + let out_dict = PyDict::new_bound(py); + for (qubit, indices) in self + .qubit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Qubit(idx as u32), indices)) + { + out_dict.set_item( + self.qubits.get(qubit).unwrap().clone_ref(py), + self.get_node(py, indices[1])?, + )?; + } + for (clbit, indices) in self + .clbit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Clbit(idx as u32), indices)) + { + out_dict.set_item( + self.clbits.get(clbit).unwrap().clone_ref(py), + self.get_node(py, indices[1])?, + )?; + } + for (var, index) in self.var_output_map.dict.bind(py).iter() { + out_dict.set_item( + var, + self.get_node(py, NodeIndex::new(index.extract::()?))?, + )?; + } + Ok(out_dict.unbind()) + } + + fn __getstate__(&self, py: Python) -> PyResult> { + let out_dict = PyDict::new_bound(py); + out_dict.set_item("name", self.name.as_ref().map(|x| x.clone_ref(py)))?; + out_dict.set_item("metadata", self.metadata.as_ref().map(|x| x.clone_ref(py)))?; + out_dict.set_item("calibrations", self.calibrations.clone())?; + out_dict.set_item("qregs", self.qregs.clone_ref(py))?; + out_dict.set_item("cregs", self.cregs.clone_ref(py))?; + out_dict.set_item("global_phase", self.global_phase.clone())?; + out_dict.set_item( + "qubit_io_map", + self.qubit_io_map + .iter() + .enumerate() + .map(|(k, v)| (k, [v[0].index(), v[1].index()])) + .collect::>(), + )?; + out_dict.set_item( + "clbit_io_map", + self.clbit_io_map + .iter() + .enumerate() + .map(|(k, v)| (k, [v[0].index(), v[1].index()])) + .collect::>(), + )?; + out_dict.set_item("var_input_map", self.var_input_map.dict.clone_ref(py))?; + out_dict.set_item("var_output_map", self.var_output_map.dict.clone_ref(py))?; + out_dict.set_item("op_name", self.op_names.clone())?; + out_dict.set_item( + "vars_info", + self.vars_info + .iter() + .map(|(k, v)| { + ( + k, + ( + v.var.clone_ref(py), + v.type_ as u8, + v.in_node.index(), + v.out_node.index(), + ), + ) + }) + .collect::>(), + )?; + out_dict.set_item("vars_by_type", self.vars_by_type.clone())?; + out_dict.set_item("qubits", self.qubits.bits())?; + out_dict.set_item("clbits", self.clbits.bits())?; + let mut nodes: Vec = Vec::with_capacity(self.dag.node_count()); + for node_idx in self.dag.node_indices() { + let node_data = self.get_node(py, node_idx)?; + nodes.push((node_idx.index(), node_data).to_object(py)); + } + out_dict.set_item("nodes", nodes)?; + out_dict.set_item( + "nodes_removed", + self.dag.node_count() != self.dag.node_bound(), + )?; + let mut edges: Vec = Vec::with_capacity(self.dag.edge_bound()); + // edges are saved with none (deleted edges) instead of their index to save space + for i in 0..self.dag.edge_bound() { + let idx = EdgeIndex::new(i); + let edge = match self.dag.edge_weight(idx) { + Some(edge_w) => { + let endpoints = self.dag.edge_endpoints(idx).unwrap(); + ( + endpoints.0.index(), + endpoints.1.index(), + edge_w.clone().to_pickle(py), + ) + .to_object(py) + } + None => py.None(), + }; + edges.push(edge); + } + out_dict.set_item("edges", edges)?; + Ok(out_dict.unbind()) + } + + fn __setstate__(&mut self, py: Python, state: PyObject) -> PyResult<()> { + let dict_state = state.downcast_bound::(py)?; + self.name = dict_state.get_item("name")?.unwrap().extract()?; + self.metadata = dict_state.get_item("metadata")?.unwrap().extract()?; + self.calibrations = dict_state.get_item("calibrations")?.unwrap().extract()?; + self.qregs = dict_state.get_item("qregs")?.unwrap().extract()?; + self.cregs = dict_state.get_item("cregs")?.unwrap().extract()?; + self.global_phase = dict_state.get_item("global_phase")?.unwrap().extract()?; + self.op_names = dict_state.get_item("op_name")?.unwrap().extract()?; + self.var_input_map = _VarIndexMap { + dict: dict_state.get_item("var_input_map")?.unwrap().extract()?, + }; + self.var_output_map = _VarIndexMap { + dict: dict_state.get_item("var_output_map")?.unwrap().extract()?, + }; + self.vars_by_type = dict_state.get_item("vars_by_type")?.unwrap().extract()?; + let binding = dict_state.get_item("vars_info")?.unwrap(); + let vars_info_raw = binding.downcast::().unwrap(); + self.vars_info = HashMap::with_capacity(vars_info_raw.len()); + for (key, value) in vars_info_raw.iter() { + let val_tuple = value.downcast::()?; + let info = DAGVarInfo { + var: val_tuple.get_item(0)?.unbind(), + type_: match val_tuple.get_item(1)?.extract::()? { + 0 => DAGVarType::Input, + 1 => DAGVarType::Capture, + 2 => DAGVarType::Declare, + _ => return Err(PyValueError::new_err("Invalid var type")), + }, + in_node: NodeIndex::new(val_tuple.get_item(2)?.extract()?), + out_node: NodeIndex::new(val_tuple.get_item(3)?.extract()?), + }; + self.vars_info.insert(key.extract()?, info); + } + + let binding = dict_state.get_item("qubits")?.unwrap(); + let qubits_raw = binding.downcast::().unwrap(); + for bit in qubits_raw.iter() { + self.qubits.add(py, &bit, false)?; + } + let binding = dict_state.get_item("clbits")?.unwrap(); + let clbits_raw = binding.downcast::().unwrap(); + for bit in clbits_raw.iter() { + self.clbits.add(py, &bit, false)?; + } + let binding = dict_state.get_item("qubit_io_map")?.unwrap(); + let qubit_index_map_raw = binding.downcast::().unwrap(); + self.qubit_io_map = Vec::with_capacity(qubit_index_map_raw.len()); + for (_k, v) in qubit_index_map_raw.iter() { + let indices: [usize; 2] = v.extract()?; + self.qubit_io_map + .push([NodeIndex::new(indices[0]), NodeIndex::new(indices[1])]); + } + let binding = dict_state.get_item("clbit_io_map")?.unwrap(); + let clbit_index_map_raw = binding.downcast::().unwrap(); + self.clbit_io_map = Vec::with_capacity(clbit_index_map_raw.len()); + + for (_k, v) in clbit_index_map_raw.iter() { + let indices: [usize; 2] = v.extract()?; + self.clbit_io_map + .push([NodeIndex::new(indices[0]), NodeIndex::new(indices[1])]); + } + // Rebuild Graph preserving index holes: + let binding = dict_state.get_item("nodes")?.unwrap(); + let nodes_lst = binding.downcast::()?; + let binding = dict_state.get_item("edges")?.unwrap(); + let edges_lst = binding.downcast::()?; + let node_removed: bool = dict_state.get_item("nodes_removed")?.unwrap().extract()?; + self.dag = StableDiGraph::default(); + if !node_removed { + for item in nodes_lst.iter() { + let node_w = item.downcast::().unwrap().get_item(1).unwrap(); + let weight = self.pack_into(py, &node_w)?; + self.dag.add_node(weight); + } + } else if nodes_lst.len() == 1 { + // graph has only one node, handle logic here to save one if in the loop later + let binding = nodes_lst.get_item(0).unwrap(); + let item = binding.downcast::().unwrap(); + let node_idx: usize = item.get_item(0).unwrap().extract().unwrap(); + let node_w = item.get_item(1).unwrap(); + + for _i in 0..node_idx { + self.dag.add_node(NodeType::QubitIn(Qubit(u32::MAX))); + } + let weight = self.pack_into(py, &node_w)?; + self.dag.add_node(weight); + for i in 0..node_idx { + self.dag.remove_node(NodeIndex::new(i)); + } + } else { + let binding = nodes_lst.get_item(nodes_lst.len() - 1).unwrap(); + let last_item = binding.downcast::().unwrap(); + + // list of temporary nodes that will be removed later to re-create holes + let node_bound_1: usize = last_item.get_item(0).unwrap().extract().unwrap(); + let mut tmp_nodes: Vec = + Vec::with_capacity(node_bound_1 + 1 - nodes_lst.len()); + + for item in nodes_lst { + let item = item.downcast::().unwrap(); + let next_index: usize = item.get_item(0).unwrap().extract().unwrap(); + let weight: PyObject = item.get_item(1).unwrap().extract().unwrap(); + while next_index > self.dag.node_bound() { + // node does not exist + let tmp_node = self.dag.add_node(NodeType::QubitIn(Qubit(u32::MAX))); + tmp_nodes.push(tmp_node); + } + // add node to the graph, and update the next available node index + let weight = self.pack_into(py, weight.bind(py))?; + self.dag.add_node(weight); + } + // Remove any temporary nodes we added + for tmp_node in tmp_nodes { + self.dag.remove_node(tmp_node); + } + } + + // to ensure O(1) on edge deletion, use a temporary node to store missing edges + let tmp_node = self.dag.add_node(NodeType::QubitIn(Qubit(u32::MAX))); + + for item in edges_lst { + if item.is_none() { + // add a temporary edge that will be deleted later to re-create the hole + self.dag + .add_edge(tmp_node, tmp_node, Wire::Qubit(Qubit(u32::MAX))); + } else { + let triple = item.downcast::().unwrap(); + let edge_p: usize = triple.get_item(0).unwrap().extract().unwrap(); + let edge_c: usize = triple.get_item(1).unwrap().extract().unwrap(); + let edge_w = Wire::from_pickle(&triple.get_item(2).unwrap())?; + self.dag + .add_edge(NodeIndex::new(edge_p), NodeIndex::new(edge_c), edge_w); + } + } + self.dag.remove_node(tmp_node); + Ok(()) + } + + /// Returns the current sequence of registered :class:`.Qubit` instances as a list. + /// + /// .. warning:: + /// + /// Do not modify this list yourself. It will invalidate the :class:`DAGCircuit` data + /// structures. + /// + /// Returns: + /// list(:class:`.Qubit`): The current sequence of registered qubits. + #[getter] + pub fn qubits(&self, py: Python<'_>) -> Py { + self.qubits.cached().clone_ref(py) + } + + /// Returns the current sequence of registered :class:`.Clbit` + /// instances as a list. + /// + /// .. warning:: + /// + /// Do not modify this list yourself. It will invalidate the :class:`DAGCircuit` data + /// structures. + /// + /// Returns: + /// list(:class:`.Clbit`): The current sequence of registered clbits. + #[getter] + pub fn clbits(&self, py: Python<'_>) -> Py { + self.clbits.cached().clone_ref(py) + } + + /// Return a list of the wires in order. + #[getter] + fn get_wires(&self, py: Python<'_>) -> PyResult> { + let wires: Vec<&PyObject> = self + .qubits + .bits() + .iter() + .chain(self.clbits.bits().iter()) + .collect(); + let out_list = PyList::new_bound(py, wires); + for var_type_set in &self.vars_by_type { + for var in var_type_set.bind(py).iter() { + out_list.append(var)?; + } + } + Ok(out_list.unbind()) + } + + /// Returns the number of nodes in the dag. + #[getter] + fn get_node_counter(&self) -> usize { + self.dag.node_count() + } + + /// Return the global phase of the circuit. + #[getter] + fn get_global_phase(&self) -> Param { + self.global_phase.clone() + } + + /// Set the global phase of the circuit. + /// + /// Args: + /// angle (float, :class:`.ParameterExpression`): The phase angle. + #[setter] + fn set_global_phase(&mut self, angle: Param) -> PyResult<()> { + match angle { + Param::Float(angle) => { + self.global_phase = Param::Float(angle.rem_euclid(2. * PI)); + } + Param::ParameterExpression(angle) => { + self.global_phase = Param::ParameterExpression(angle); + } + Param::Obj(_) => return Err(PyTypeError::new_err("Invalid type for global phase")), + } + Ok(()) + } + + /// Return calibration dictionary. + /// + /// The custom pulse definition of a given gate is of the form + /// {'gate_name': {(qubits, params): schedule}} + #[getter] + fn get_calibrations(&self) -> HashMap> { + self.calibrations.clone() + } + + /// Set the circuit calibration data from a dictionary of calibration definition. + /// + /// Args: + /// calibrations (dict): A dictionary of input in the format + /// {'gate_name': {(qubits, gate_params): schedule}} + #[setter] + fn set_calibrations(&mut self, calibrations: HashMap>) { + self.calibrations = calibrations; + } + + /// Register a low-level, custom pulse definition for the given gate. + /// + /// Args: + /// gate (Union[Gate, str]): Gate information. + /// qubits (Union[int, Tuple[int]]): List of qubits to be measured. + /// schedule (Schedule): Schedule information. + /// params (Optional[List[Union[float, Parameter]]]): A list of parameters. + /// + /// Raises: + /// Exception: if the gate is of type string and params is None. + fn add_calibration<'py>( + &mut self, + py: Python<'py>, + mut gate: Bound<'py, PyAny>, + qubits: Bound<'py, PyAny>, + schedule: Py, + mut params: Option>, + ) -> PyResult<()> { + if gate.is_instance(imports::GATE.get_bound(py))? { + params = Some(gate.getattr(intern!(py, "params"))?); + gate = gate.getattr(intern!(py, "name"))?; + } + + let params_tuple = if let Some(operands) = params { + let add_calibration = PyModule::from_code_bound( + py, + r#" +import numpy as np + +def _format(operand): + try: + # Using float/complex value as a dict key is not good idea. + # This makes the mapping quite sensitive to the rounding error. + # However, the mechanism is already tied to the execution model (i.e. pulse gate) + # and we cannot easily update this rule. + # The same logic exists in QuantumCircuit.add_calibration. + evaluated = complex(operand) + if np.isreal(evaluated): + evaluated = float(evaluated.real) + if evaluated.is_integer(): + evaluated = int(evaluated) + return evaluated + except TypeError: + # Unassigned parameter + return operand + "#, + "add_calibration.py", + "add_calibration", + )?; + + let format = add_calibration.getattr("_format")?; + let mapped: PyResult> = operands.iter()?.map(|p| format.call1((p?,))).collect(); + PyTuple::new_bound(py, mapped?).into_any() + } else { + PyTuple::empty_bound(py).into_any() + }; + + let calibrations = self + .calibrations + .entry(gate.extract()?) + .or_insert_with(|| PyDict::new_bound(py).unbind()) + .bind(py); + + let qubits = if let Ok(qubits) = qubits.downcast::() { + qubits.to_tuple()?.into_any() + } else { + PyTuple::new_bound(py, [qubits]).into_any() + }; + let key = PyTuple::new_bound(py, &[qubits.unbind(), params_tuple.into_any().unbind()]); + calibrations.set_item(key, schedule)?; + Ok(()) + } + + /// Return True if the dag has a calibration defined for the node operation. In this + /// case, the operation does not need to be translated to the device basis. + fn has_calibration_for(&self, py: Python, node: PyRef) -> PyResult { + if !self + .calibrations + .contains_key(node.instruction.operation.name()) + { + return Ok(false); + } + let mut params = Vec::new(); + for p in &node.instruction.params { + if let Param::ParameterExpression(exp) = p { + let exp = exp.bind(py); + if !exp.getattr(intern!(py, "parameters"))?.is_truthy()? { + let as_py_float = exp.call_method0(intern!(py, "__float__"))?; + params.push(as_py_float.unbind()); + continue; + } + } + params.push(p.to_object(py)); + } + let qubits: Vec = self + .qubits + .map_bits(node.instruction.qubits.bind(py).iter())? + .map(|bit| bit.0) + .collect(); + let qubits = PyTuple::new_bound(py, qubits); + let params = PyTuple::new_bound(py, params); + self.calibrations[node.instruction.operation.name()] + .bind(py) + .contains((qubits, params).to_object(py)) + } + + /// Remove all operation nodes with the given name. + fn remove_all_ops_named(&mut self, opname: &str) { + let mut to_remove = Vec::new(); + for (id, weight) in self.dag.node_references() { + if let NodeType::Operation(packed) = &weight { + if opname == packed.op.name() { + to_remove.push(id); + } + } + } + for node in to_remove { + self.remove_op_node(node); + } + } + + /// Add individual qubit wires. + fn add_qubits(&mut self, py: Python, qubits: Vec>) -> PyResult<()> { + for bit in qubits.iter() { + if !bit.is_instance(imports::QUBIT.get_bound(py))? { + return Err(DAGCircuitError::new_err("not a Qubit instance.")); + } + + if self.qubits.find(bit).is_some() { + return Err(DAGCircuitError::new_err(format!( + "duplicate qubits {}", + bit + ))); + } + } + + for bit in qubits.iter() { + self.add_qubit_unchecked(py, bit)?; + } + Ok(()) + } + + /// Add individual qubit wires. + fn add_clbits(&mut self, py: Python, clbits: Vec>) -> PyResult<()> { + for bit in clbits.iter() { + if !bit.is_instance(imports::CLBIT.get_bound(py))? { + return Err(DAGCircuitError::new_err("not a Clbit instance.")); + } + + if self.clbits.find(bit).is_some() { + return Err(DAGCircuitError::new_err(format!( + "duplicate clbits {}", + bit + ))); + } + } + + for bit in clbits.iter() { + self.add_clbit_unchecked(py, bit)?; + } + Ok(()) + } + + /// Add all wires in a quantum register. + fn add_qreg(&mut self, py: Python, qreg: &Bound) -> PyResult<()> { + if !qreg.is_instance(imports::QUANTUM_REGISTER.get_bound(py))? { + return Err(DAGCircuitError::new_err("not a QuantumRegister instance.")); + } + + let register_name = qreg.getattr(intern!(py, "name"))?; + if self.qregs.bind(py).contains(®ister_name)? { + return Err(DAGCircuitError::new_err(format!( + "duplicate register {}", + register_name + ))); + } + self.qregs.bind(py).set_item(®ister_name, qreg)?; + + for (index, bit) in qreg.iter()?.enumerate() { + let bit = bit?; + if self.qubits.find(&bit).is_none() { + self.add_qubit_unchecked(py, &bit)?; + } + let locations: PyRef = self + .qubit_locations + .bind(py) + .get_item(&bit)? + .unwrap() + .extract()?; + locations.registers.bind(py).append((qreg, index))?; + } + Ok(()) + } + + /// Add all wires in a classical register. + fn add_creg(&mut self, py: Python, creg: &Bound) -> PyResult<()> { + if !creg.is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? { + return Err(DAGCircuitError::new_err( + "not a ClassicalRegister instance.", + )); + } + + let register_name = creg.getattr(intern!(py, "name"))?; + if self.cregs.bind(py).contains(®ister_name)? { + return Err(DAGCircuitError::new_err(format!( + "duplicate register {}", + register_name + ))); + } + self.cregs.bind(py).set_item(register_name, creg)?; + + for (index, bit) in creg.iter()?.enumerate() { + let bit = bit?; + if self.clbits.find(&bit).is_none() { + self.add_clbit_unchecked(py, &bit)?; + } + let locations: PyRef = self + .clbit_locations + .bind(py) + .get_item(&bit)? + .unwrap() + .extract()?; + locations.registers.bind(py).append((creg, index))?; + } + Ok(()) + } + + /// Finds locations in the circuit, by mapping the Qubit and Clbit to positional index + /// BitLocations is defined as: BitLocations = namedtuple("BitLocations", ("index", "registers")) + /// + /// Args: + /// bit (Bit): The bit to locate. + /// + /// Returns: + /// namedtuple(int, List[Tuple(Register, int)]): A 2-tuple. The first element (``index``) + /// contains the index at which the ``Bit`` can be found (in either + /// :obj:`~DAGCircuit.qubits`, :obj:`~DAGCircuit.clbits`, depending on its + /// type). The second element (``registers``) is a list of ``(register, index)`` + /// pairs with an entry for each :obj:`~Register` in the circuit which contains the + /// :obj:`~Bit` (and the index in the :obj:`~Register` at which it can be found). + /// + /// Raises: + /// DAGCircuitError: If the supplied :obj:`~Bit` was of an unknown type. + /// DAGCircuitError: If the supplied :obj:`~Bit` could not be found on the circuit. + fn find_bit<'py>(&self, py: Python<'py>, bit: &Bound) -> PyResult> { + if bit.is_instance(imports::QUBIT.get_bound(py))? { + return self.qubit_locations.bind(py).get_item(bit)?.ok_or_else(|| { + DAGCircuitError::new_err(format!( + "Could not locate provided bit: {}. Has it been added to the DAGCircuit?", + bit + )) + }); + } + + if bit.is_instance(imports::CLBIT.get_bound(py))? { + return self.clbit_locations.bind(py).get_item(bit)?.ok_or_else(|| { + DAGCircuitError::new_err(format!( + "Could not locate provided bit: {}. Has it been added to the DAGCircuit?", + bit + )) + }); + } + + Err(DAGCircuitError::new_err(format!( + "Could not locate bit of unknown type: {}", + bit.get_type() + ))) + } + + /// Remove classical bits from the circuit. All bits MUST be idle. + /// Any registers with references to at least one of the specified bits will + /// also be removed. + /// + /// .. warning:: + /// This method is rather slow, since it must iterate over the entire + /// DAG to fix-up bit indices. + /// + /// Args: + /// clbits (List[Clbit]): The bits to remove. + /// + /// Raises: + /// DAGCircuitError: a clbit is not a :obj:`.Clbit`, is not in the circuit, + /// or is not idle. + #[pyo3(signature = (*clbits))] + fn remove_clbits(&mut self, py: Python, clbits: &Bound) -> PyResult<()> { + let mut non_bits = Vec::new(); + for bit in clbits.iter() { + if !bit.is_instance(imports::CLBIT.get_bound(py))? { + non_bits.push(bit); + } + } + if !non_bits.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "clbits not of type Clbit: {:?}", + non_bits + ))); + } + + let bit_iter = match self.clbits.map_bits(clbits.iter()) { + Ok(bit_iter) => bit_iter, + Err(_) => { + return Err(DAGCircuitError::new_err(format!( + "clbits not in circuit: {:?}", + clbits + ))) + } + }; + let clbits: HashSet = bit_iter.collect(); + let mut busy_bits = Vec::new(); + for bit in clbits.iter() { + if !self.is_wire_idle(py, &Wire::Clbit(*bit))? { + busy_bits.push(self.clbits.get(*bit).unwrap()); + } + } + + if !busy_bits.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "clbits not idle: {:?}", + busy_bits + ))); + } + + // Remove any references to bits. + let mut cregs_to_remove = Vec::new(); + for creg in self.cregs.bind(py).values() { + for bit in creg.iter()? { + let bit = bit?; + if clbits.contains(&self.clbits.find(&bit).unwrap()) { + cregs_to_remove.push(creg); + break; + } + } + } + self.remove_cregs(py, &PyTuple::new_bound(py, cregs_to_remove))?; + + // Remove DAG in/out nodes etc. + for bit in clbits.iter() { + self.remove_idle_wire(py, Wire::Clbit(*bit))?; + } + + // Copy the current clbit mapping so we can use it while remapping + // wires used on edges and in operation cargs. + let old_clbits = self.clbits.clone(); + + // Remove the clbit indices, which will invalidate our mapping of Clbit to + // Python bits throughout the entire DAG. + self.clbits.remove_indices(py, clbits.clone())?; + + // Update input/output maps to use new Clbits. + let io_mapping: HashMap = self + .clbit_io_map + .drain(..) + .enumerate() + .filter_map(|(k, v)| { + let clbit = Clbit(k as u32); + if clbits.contains(&clbit) { + None + } else { + Some(( + self.clbits + .find(old_clbits.get(Clbit(k as u32)).unwrap().bind(py)) + .unwrap(), + v, + )) + } + }) + .collect(); + + self.clbit_io_map = (0..io_mapping.len()) + .map(|idx| { + let clbit = Clbit(idx as u32); + io_mapping[&clbit] + }) + .collect(); + + // Update edges to use the new Clbits. + for edge_weight in self.dag.edge_weights_mut() { + if let Wire::Clbit(c) = edge_weight { + *c = self + .clbits + .find(old_clbits.get(*c).unwrap().bind(py)) + .unwrap(); + } + } + + // Update operation cargs to use the new Clbits. + for node_weight in self.dag.node_weights_mut() { + match node_weight { + NodeType::Operation(op) => { + let cargs = self.cargs_cache.intern(op.clbits); + let carg_bits = old_clbits + .map_indices(&cargs[..]) + .map(|b| b.bind(py).clone()); + let mapped_cargs = self.clbits.map_bits(carg_bits)?.collect(); + let clbits = Interner::intern(&mut self.cargs_cache, mapped_cargs)?; + op.clbits = clbits; + } + NodeType::ClbitIn(c) | NodeType::ClbitOut(c) => { + *c = self + .clbits + .find(old_clbits.get(*c).unwrap().bind(py)) + .unwrap(); + } + _ => (), + } + } + + // Update bit locations. + let bit_locations = self.clbit_locations.bind(py); + for (i, bit) in self.clbits.bits().iter().enumerate() { + let raw_loc = bit_locations.get_item(bit)?.unwrap(); + let loc = raw_loc.downcast::().unwrap(); + loc.borrow_mut().index = i; + bit_locations.set_item(bit, loc)?; + } + Ok(()) + } + + /// Remove classical registers from the circuit, leaving underlying bits + /// in place. + /// + /// Raises: + /// DAGCircuitError: a creg is not a ClassicalRegister, or is not in + /// the circuit. + #[pyo3(signature = (*cregs))] + fn remove_cregs(&mut self, py: Python, cregs: &Bound) -> PyResult<()> { + let mut non_regs = Vec::new(); + let mut unknown_regs = Vec::new(); + let self_bound_cregs = self.cregs.bind(py); + for reg in cregs.iter() { + if !reg.is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? { + non_regs.push(reg); + } else if let Some(existing_creg) = + self_bound_cregs.get_item(®.getattr(intern!(py, "name"))?)? + { + if !existing_creg.eq(®)? { + unknown_regs.push(reg); + } + } else { + unknown_regs.push(reg); + } + } + if !non_regs.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "cregs not of type ClassicalRegister: {:?}", + non_regs + ))); + } + if !unknown_regs.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "cregs not in circuit: {:?}", + unknown_regs + ))); + } + + for creg in cregs { + self.cregs + .bind(py) + .del_item(creg.getattr(intern!(py, "name"))?)?; + for (i, bit) in creg.iter()?.enumerate() { + let bit = bit?; + let bit_position = self + .clbit_locations + .bind(py) + .get_item(bit)? + .unwrap() + .downcast_into_exact::()?; + bit_position + .borrow() + .registers + .bind(py) + .as_any() + .call_method1(intern!(py, "remove"), ((&creg, i),))?; + } + } + Ok(()) + } + + /// Remove quantum bits from the circuit. All bits MUST be idle. + /// Any registers with references to at least one of the specified bits will + /// also be removed. + /// + /// .. warning:: + /// This method is rather slow, since it must iterate over the entire + /// DAG to fix-up bit indices. + /// + /// Args: + /// qubits (List[~qiskit.circuit.Qubit]): The bits to remove. + /// + /// Raises: + /// DAGCircuitError: a qubit is not a :obj:`~.circuit.Qubit`, is not in the circuit, + /// or is not idle. + #[pyo3(signature = (*qubits))] + fn remove_qubits(&mut self, py: Python, qubits: &Bound) -> PyResult<()> { + let mut non_qbits = Vec::new(); + for bit in qubits.iter() { + if !bit.is_instance(imports::QUBIT.get_bound(py))? { + non_qbits.push(bit); + } + } + if !non_qbits.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "qubits not of type Qubit: {:?}", + non_qbits + ))); + } + + let bit_iter = match self.qubits.map_bits(qubits.iter()) { + Ok(bit_iter) => bit_iter, + Err(_) => { + return Err(DAGCircuitError::new_err(format!( + "qubits not in circuit: {:?}", + qubits + ))) + } + }; + let qubits: HashSet = bit_iter.collect(); + + let mut busy_bits = Vec::new(); + for bit in qubits.iter() { + if !self.is_wire_idle(py, &Wire::Qubit(*bit))? { + busy_bits.push(self.qubits.get(*bit).unwrap()); + } + } + + if !busy_bits.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "qubits not idle: {:?}", + busy_bits + ))); + } + + // Remove any references to bits. + let mut qregs_to_remove = Vec::new(); + for qreg in self.qregs.bind(py).values() { + for bit in qreg.iter()? { + let bit = bit?; + if qubits.contains(&self.qubits.find(&bit).unwrap()) { + qregs_to_remove.push(qreg); + break; + } + } + } + self.remove_qregs(py, &PyTuple::new_bound(py, qregs_to_remove))?; + + // Remove DAG in/out nodes etc. + for bit in qubits.iter() { + self.remove_idle_wire(py, Wire::Qubit(*bit))?; + } + + // Copy the current qubit mapping so we can use it while remapping + // wires used on edges and in operation qargs. + let old_qubits = self.qubits.clone(); + + // Remove the qubit indices, which will invalidate our mapping of Qubit to + // Python bits throughout the entire DAG. + self.qubits.remove_indices(py, qubits.clone())?; + + // Update input/output maps to use new Qubits. + let io_mapping: HashMap = self + .qubit_io_map + .drain(..) + .enumerate() + .filter_map(|(k, v)| { + let qubit = Qubit(k as u32); + if qubits.contains(&qubit) { + None + } else { + Some(( + self.qubits + .find(old_qubits.get(qubit).unwrap().bind(py)) + .unwrap(), + v, + )) + } + }) + .collect(); + + self.qubit_io_map = (0..io_mapping.len()) + .map(|idx| { + let qubit = Qubit(idx as u32); + io_mapping[&qubit] + }) + .collect(); + + // Update edges to use the new Qubits. + for edge_weight in self.dag.edge_weights_mut() { + if let Wire::Qubit(b) = edge_weight { + *b = self + .qubits + .find(old_qubits.get(*b).unwrap().bind(py)) + .unwrap(); + } + } + + // Update operation qargs to use the new Qubits. + for node_weight in self.dag.node_weights_mut() { + match node_weight { + NodeType::Operation(op) => { + let qargs = self.qargs_cache.intern(op.qubits); + let qarg_bits = old_qubits + .map_indices(&qargs[..]) + .map(|b| b.bind(py).clone()); + let mapped_qargs = self.qubits.map_bits(qarg_bits)?.collect(); + let qubits = Interner::intern(&mut self.qargs_cache, mapped_qargs)?; + op.qubits = qubits; + } + NodeType::QubitIn(q) | NodeType::QubitOut(q) => { + *q = self + .qubits + .find(old_qubits.get(*q).unwrap().bind(py)) + .unwrap(); + } + _ => (), + } + } + + // Update bit locations. + let bit_locations = self.qubit_locations.bind(py); + for (i, bit) in self.qubits.bits().iter().enumerate() { + let raw_loc = bit_locations.get_item(bit)?.unwrap(); + let loc = raw_loc.downcast::().unwrap(); + loc.borrow_mut().index = i; + bit_locations.set_item(bit, loc)?; + } + Ok(()) + } + + /// Remove quantum registers from the circuit, leaving underlying bits + /// in place. + /// + /// Raises: + /// DAGCircuitError: a qreg is not a QuantumRegister, or is not in + /// the circuit. + #[pyo3(signature = (*qregs))] + fn remove_qregs(&mut self, py: Python, qregs: &Bound) -> PyResult<()> { + let mut non_regs = Vec::new(); + let mut unknown_regs = Vec::new(); + let self_bound_qregs = self.qregs.bind(py); + for reg in qregs.iter() { + if !reg.is_instance(imports::QUANTUM_REGISTER.get_bound(py))? { + non_regs.push(reg); + } else if let Some(existing_qreg) = + self_bound_qregs.get_item(®.getattr(intern!(py, "name"))?)? + { + if !existing_qreg.eq(®)? { + unknown_regs.push(reg); + } + } else { + unknown_regs.push(reg); + } + } + if !non_regs.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "qregs not of type QuantumRegister: {:?}", + non_regs + ))); + } + if !unknown_regs.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "qregs not in circuit: {:?}", + unknown_regs + ))); + } + + for qreg in qregs { + self.qregs + .bind(py) + .del_item(qreg.getattr(intern!(py, "name"))?)?; + for (i, bit) in qreg.iter()?.enumerate() { + let bit = bit?; + let bit_position = self + .qubit_locations + .bind(py) + .get_item(bit)? + .unwrap() + .downcast_into_exact::()?; + bit_position + .borrow() + .registers + .bind(py) + .as_any() + .call_method1(intern!(py, "remove"), ((&qreg, i),))?; + } + } + Ok(()) + } + + /// Verify that the condition is valid. + /// + /// Args: + /// name (string): used for error reporting + /// condition (tuple or None): a condition tuple (ClassicalRegister, int) or (Clbit, bool) + /// + /// Raises: + /// DAGCircuitError: if conditioning on an invalid register + fn _check_condition(&self, py: Python, name: &str, condition: &Bound) -> PyResult<()> { + if condition.is_none() { + return Ok(()); + } + + let resources = self.control_flow_module.condition_resources(condition)?; + for reg in resources.cregs.bind(py) { + if !self + .cregs + .bind(py) + .contains(reg.getattr(intern!(py, "name"))?)? + { + return Err(DAGCircuitError::new_err(format!( + "invalid creg in condition for {}", + name + ))); + } + } + + for bit in resources.clbits.bind(py) { + if self.clbits.find(&bit).is_none() { + return Err(DAGCircuitError::new_err(format!( + "invalid clbits in condition for {}", + name + ))); + } + } + + Ok(()) + } + + /// Return a copy of self with the same structure but empty. + /// + /// That structure includes: + /// * name and other metadata + /// * global phase + /// * duration + /// * all the qubits and clbits, including the registers. + /// + /// Returns: + /// DAGCircuit: An empty copy of self. + #[pyo3(signature = (*, vars_mode="alike"))] + fn copy_empty_like(&self, py: Python, vars_mode: &str) -> PyResult { + let mut target_dag = DAGCircuit::new(py)?; + target_dag.name = self.name.as_ref().map(|n| n.clone_ref(py)); + target_dag.global_phase = self.global_phase.clone(); + target_dag.duration = self.duration.as_ref().map(|d| d.clone_ref(py)); + target_dag.unit.clone_from(&self.unit); + target_dag.metadata = self.metadata.as_ref().map(|m| m.clone_ref(py)); + target_dag.qargs_cache = self.qargs_cache.clone(); + target_dag.cargs_cache = self.cargs_cache.clone(); + + for bit in self.qubits.bits() { + target_dag.add_qubit_unchecked(py, bit.bind(py))?; + } + for bit in self.clbits.bits() { + target_dag.add_clbit_unchecked(py, bit.bind(py))?; + } + for reg in self.qregs.bind(py).values() { + target_dag.add_qreg(py, ®)?; + } + for reg in self.cregs.bind(py).values() { + target_dag.add_creg(py, ®)?; + } + if vars_mode == "alike" { + for var in self.vars_by_type[DAGVarType::Input as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Input)?; + } + for var in self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Capture)?; + } + for var in self.vars_by_type[DAGVarType::Declare as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Declare)?; + } + } else if vars_mode == "captures" { + for var in self.vars_by_type[DAGVarType::Input as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Capture)?; + } + for var in self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Capture)?; + } + for var in self.vars_by_type[DAGVarType::Declare as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Capture)?; + } + } else if vars_mode != "drop" { + return Err(PyValueError::new_err(format!( + "unknown vars_mode: '{}'", + vars_mode + ))); + } + + Ok(target_dag) + } + + #[pyo3(signature=(node, check=false))] + fn _apply_op_node_back( + &mut self, + py: Python, + node: &Bound, + check: bool, + ) -> PyResult<()> { + if let NodeType::Operation(inst) = self.pack_into(py, node)? { + if check { + self.check_op_addition(py, &inst)?; + } + + self.push_back(py, inst)?; + Ok(()) + } else { + Err(PyTypeError::new_err("Invalid node type input")) + } + } + + /// Apply an operation to the output of the circuit. + /// + /// Args: + /// op (qiskit.circuit.Operation): the operation associated with the DAG node + /// qargs (tuple[~qiskit.circuit.Qubit]): qubits that op will be applied to + /// cargs (tuple[Clbit]): cbits that op will be applied to + /// check (bool): If ``True`` (default), this function will enforce that the + /// :class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are + /// :class:`~.circuit.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must* + /// uphold these invariants itself, but the cost of several checks will be skipped. + /// This is most useful when building a new DAG from a source of known-good nodes. + /// Returns: + /// DAGOpNode: the node for the op that was added to the dag + /// + /// Raises: + /// DAGCircuitError: if a leaf node is connected to multiple outputs + #[pyo3(name = "apply_operation_back", signature = (op, qargs=None, cargs=None, *, check=true))] + fn py_apply_operation_back( + &mut self, + py: Python, + op: Bound, + qargs: Option, + cargs: Option, + check: bool, + ) -> PyResult> { + let py_op = op.extract::()?; + let qargs = qargs.map(|q| q.value); + let cargs = cargs.map(|c| c.value); + let node = { + let qubits_id = Interner::intern( + &mut self.qargs_cache, + self.qubits.map_bits(qargs.iter().flatten())?.collect(), + )?; + let clbits_id = Interner::intern( + &mut self.cargs_cache, + self.clbits.map_bits(cargs.iter().flatten())?.collect(), + )?; + let instr = PackedInstruction { + op: py_op.operation, + qubits: qubits_id, + clbits: clbits_id, + params: (!py_op.params.is_empty()).then(|| Box::new(py_op.params)), + extra_attrs: py_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: op.unbind().into(), + }; + + if check { + self.check_op_addition(py, &instr)?; + } + self.push_back(py, instr)? + }; + + self.get_node(py, node) + } + + /// Apply an operation to the input of the circuit. + /// + /// Args: + /// op (qiskit.circuit.Operation): the operation associated with the DAG node + /// qargs (tuple[~qiskit.circuit.Qubit]): qubits that op will be applied to + /// cargs (tuple[Clbit]): cbits that op will be applied to + /// check (bool): If ``True`` (default), this function will enforce that the + /// :class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are + /// :class:`~.circuit.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must* + /// uphold these invariants itself, but the cost of several checks will be skipped. + /// This is most useful when building a new DAG from a source of known-good nodes. + /// Returns: + /// DAGOpNode: the node for the op that was added to the dag + /// + /// Raises: + /// DAGCircuitError: if initial nodes connected to multiple out edges + #[pyo3(name = "apply_operation_front", signature = (op, qargs=None, cargs=None, *, check=true))] + fn py_apply_operation_front( + &mut self, + py: Python, + op: Bound, + qargs: Option, + cargs: Option, + check: bool, + ) -> PyResult> { + let py_op = op.extract::()?; + let qargs = qargs.map(|q| q.value); + let cargs = cargs.map(|c| c.value); + let node = { + let qubits_id = Interner::intern( + &mut self.qargs_cache, + self.qubits.map_bits(qargs.iter().flatten())?.collect(), + )?; + let clbits_id = Interner::intern( + &mut self.cargs_cache, + self.clbits.map_bits(cargs.iter().flatten())?.collect(), + )?; + let instr = PackedInstruction { + op: py_op.operation, + qubits: qubits_id, + clbits: clbits_id, + params: (!py_op.params.is_empty()).then(|| Box::new(py_op.params)), + extra_attrs: py_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: op.unbind().into(), + }; + + if check { + self.check_op_addition(py, &instr)?; + } + self.push_front(py, instr)? + }; + + self.get_node(py, node) + } + + /// Compose the ``other`` circuit onto the output of this circuit. + /// + /// A subset of input wires of ``other`` are mapped + /// to a subset of output wires of this circuit. + /// + /// ``other`` can be narrower or of equal width to ``self``. + /// + /// Args: + /// other (DAGCircuit): circuit to compose with self + /// qubits (list[~qiskit.circuit.Qubit|int]): qubits of self to compose onto. + /// clbits (list[Clbit|int]): clbits of self to compose onto. + /// front (bool): If True, front composition will be performed (not implemented yet) + /// inplace (bool): If True, modify the object. Otherwise return composed circuit. + /// inline_captures (bool): If ``True``, variables marked as "captures" in the ``other`` DAG + /// will be inlined onto existing uses of those same variables in ``self``. If ``False``, + /// all variables in ``other`` are required to be distinct from ``self``, and they will + /// be added to ``self``. + /// + /// .. + /// Note: unlike `QuantumCircuit.compose`, there's no `var_remap` argument here. That's + /// because the `DAGCircuit` inner-block structure isn't set up well to allow the recursion, + /// and `DAGCircuit.compose` is generally only used to rebuild a DAG from layers within + /// itself than to join unrelated circuits. While there's no strong motivating use-case + /// (unlike the `QuantumCircuit` equivalent), it's safer and more performant to not provide + /// the option. + /// + /// Returns: + /// DAGCircuit: the composed dag (returns None if inplace==True). + /// + /// Raises: + /// DAGCircuitError: if ``other`` is wider or there are duplicate edge mappings. + #[allow(clippy::too_many_arguments)] + #[pyo3(signature = (other, qubits=None, clbits=None, front=false, inplace=true, *, inline_captures=false))] + fn compose( + slf: PyRefMut, + py: Python, + other: &DAGCircuit, + qubits: Option>, + clbits: Option>, + front: bool, + inplace: bool, + inline_captures: bool, + ) -> PyResult> { + if front { + return Err(DAGCircuitError::new_err( + "Front composition not supported yet.", + )); + } + + if other.qubits.len() > slf.qubits.len() || other.clbits.len() > slf.clbits.len() { + return Err(DAGCircuitError::new_err( + "Trying to compose with another DAGCircuit which has more 'in' edges.", + )); + } + + // Number of qubits and clbits must match number in circuit or None + let identity_qubit_map = other + .qubits + .bits() + .iter() + .zip(slf.qubits.bits()) + .into_py_dict_bound(py); + let identity_clbit_map = other + .clbits + .bits() + .iter() + .zip(slf.clbits.bits()) + .into_py_dict_bound(py); + + let qubit_map: Bound = match qubits { + None => identity_qubit_map.clone(), + Some(qubits) => { + if qubits.len() != other.qubits.len() { + return Err(DAGCircuitError::new_err(concat!( + "Number of items in qubits parameter does not", + " match number of qubits in the circuit." + ))); + } + + let self_qubits = slf.qubits.cached().bind(py); + let other_qubits = other.qubits.cached().bind(py); + let dict = PyDict::new_bound(py); + for (i, q) in qubits.iter().enumerate() { + let q = if q.is_instance_of::() { + self_qubits.get_item(q.extract()?)? + } else { + q + }; + + dict.set_item(other_qubits.get_item(i)?, q)?; + } + dict + } + }; + + let clbit_map: Bound = match clbits { + None => identity_clbit_map.clone(), + Some(clbits) => { + if clbits.len() != other.clbits.len() { + return Err(DAGCircuitError::new_err(concat!( + "Number of items in clbits parameter does not", + " match number of clbits in the circuit." + ))); + } + + let self_clbits = slf.clbits.cached().bind(py); + let other_clbits = other.clbits.cached().bind(py); + let dict = PyDict::new_bound(py); + for (i, q) in clbits.iter().enumerate() { + let q = if q.is_instance_of::() { + self_clbits.get_item(q.extract()?)? + } else { + q + }; + + dict.set_item(other_clbits.get_item(i)?, q)?; + } + dict + } + }; + + let edge_map = if qubit_map.is_empty() && clbit_map.is_empty() { + // try to do a 1-1 mapping in order + identity_qubit_map + .iter() + .chain(identity_clbit_map.iter()) + .into_py_dict_bound(py) + } else { + qubit_map + .iter() + .chain(clbit_map.iter()) + .into_py_dict_bound(py) + }; + + // Chck duplicates in wire map. + { + let edge_map_values: Vec<_> = edge_map.values().iter().collect(); + if PySet::new_bound(py, edge_map_values.as_slice())?.len() != edge_map.len() { + return Err(DAGCircuitError::new_err("duplicates in wire_map")); + } + } + + // Compose + let mut dag: PyRefMut = if inplace { + slf + } else { + Py::new(py, slf.clone())?.into_bound(py).borrow_mut() + }; + + dag.global_phase = add_global_phase(py, &dag.global_phase, &other.global_phase)?; + + for (gate, cals) in other.calibrations.iter() { + let calibrations = match dag.calibrations.get(gate) { + Some(calibrations) => calibrations, + None => { + dag.calibrations + .insert(gate.clone(), PyDict::new_bound(py).unbind()); + &dag.calibrations[gate] + } + }; + calibrations.bind(py).update(cals.bind(py).as_mapping())?; + } + + // This is all the handling we need for realtime variables, if there's no remapping. They: + // + // * get added to the DAG and then operations involving them get appended on normally. + // * get inlined onto an existing variable, then operations get appended normally. + // * there's a clash or a failed inlining, and we just raise an error. + // + // Notably if there's no remapping, there's no need to recurse into control-flow or to do any + // Var rewriting during the Expr visits. + for var in other.iter_input_vars(py)?.bind(py) { + dag.add_input_var(py, &var?)?; + } + if inline_captures { + for var in other.iter_captured_vars(py)?.bind(py) { + let var = var?; + if !dag.has_var(&var)? { + return Err(DAGCircuitError::new_err(format!("Variable '{}' to be inlined is not in the base DAG. If you wanted it to be automatically added, use `inline_captures=False`.", var))); + } + } + } else { + for var in other.iter_captured_vars(py)?.bind(py) { + dag.add_captured_var(py, &var?)?; + } + } + for var in other.iter_declared_vars(py)?.bind(py) { + dag.add_declared_var(py, &var?)?; + } + + let variable_mapper = PyVariableMapper::new( + py, + dag.cregs.bind(py).values().into_any(), + Some(edge_map.clone()), + None, + Some(wrap_pyfunction_bound!(reject_new_register, py)?.to_object(py)), + )?; + + for node in other.topological_nodes()? { + match &other.dag[node] { + NodeType::QubitIn(q) => { + let bit = other.qubits.get(*q).unwrap().bind(py); + let m_wire = edge_map.get_item(bit)?.unwrap_or_else(|| bit.clone()); + let wire_in_dag = dag.qubits.find(&m_wire); + + if wire_in_dag.is_none() + || (dag.qubit_io_map.len() - 1 < wire_in_dag.unwrap().0 as usize) + { + return Err(DAGCircuitError::new_err(format!( + "wire {} not in self", + m_wire, + ))); + } + // TODO: Python code has check here if node.wire is in other._wires. Why? + } + NodeType::ClbitIn(c) => { + let bit = other.clbits.get(*c).unwrap().bind(py); + let m_wire = edge_map.get_item(bit)?.unwrap_or_else(|| bit.clone()); + let wire_in_dag = dag.clbits.find(&m_wire); + if wire_in_dag.is_none() + || dag.clbit_io_map.len() - 1 < wire_in_dag.unwrap().0 as usize + { + return Err(DAGCircuitError::new_err(format!( + "wire {} not in self", + m_wire, + ))); + } + // TODO: Python code has check here if node.wire is in other._wires. Why? + } + NodeType::Operation(op) => { + let m_qargs = { + let qubits = other + .qubits + .map_indices(other.qargs_cache.intern(op.qubits).as_slice()); + let mut mapped = Vec::with_capacity(qubits.len()); + for bit in qubits { + mapped.push( + edge_map + .get_item(bit)? + .unwrap_or_else(|| bit.bind(py).clone()), + ); + } + PyTuple::new_bound(py, mapped) + }; + let m_cargs = { + let clbits = other + .clbits + .map_indices(other.cargs_cache.intern(op.clbits).as_slice()); + let mut mapped = Vec::with_capacity(clbits.len()); + for bit in clbits { + mapped.push( + edge_map + .get_item(bit)? + .unwrap_or_else(|| bit.bind(py).clone()), + ); + } + PyTuple::new_bound(py, mapped) + }; + + // We explicitly create a mutable py_op here since we might + // update the condition. + let mut py_op = op.unpack_py_op(py)?.into_bound(py); + if py_op.getattr(intern!(py, "mutable"))?.extract::()? { + py_op = py_op.call_method0(intern!(py, "to_mutable"))?; + } + + if let Some(condition) = op.condition() { + // TODO: do we need to check for condition.is_none()? + let condition = variable_mapper.map_condition(condition.bind(py), true)?; + if !op.op.control_flow() { + py_op = py_op.call_method1( + intern!(py, "c_if"), + condition.downcast::()?, + )?; + } else { + py_op.setattr(intern!(py, "condition"), condition)?; + } + } else if py_op.is_instance(imports::SWITCH_CASE_OP.get_bound(py))? { + py_op.setattr( + intern!(py, "target"), + variable_mapper.map_target(&py_op.getattr(intern!(py, "target"))?)?, + )?; + }; + + dag.py_apply_operation_back( + py, + py_op, + Some(TupleLikeArg { value: m_qargs }), + Some(TupleLikeArg { value: m_cargs }), + false, + )?; + } + // If its a Var wire, we already checked that it exists in the destination. + NodeType::VarIn(_) + | NodeType::VarOut(_) + | NodeType::QubitOut(_) + | NodeType::ClbitOut(_) => (), + } + } + + if !inplace { + Ok(Some(dag.into_py(py))) + } else { + Ok(None) + } + } + + /// Reverse the operations in the ``self`` circuit. + /// + /// Returns: + /// DAGCircuit: the reversed dag. + fn reverse_ops<'py>(slf: PyRef, py: Python<'py>) -> PyResult> { + let qc = imports::DAG_TO_CIRCUIT.get_bound(py).call1((slf,))?; + let reversed = qc.call_method0("reverse_ops")?; + imports::CIRCUIT_TO_DAG.get_bound(py).call1((reversed,)) + } + + /// Return idle wires. + /// + /// Args: + /// ignore (list(str)): List of node names to ignore. Default: [] + /// + /// Yields: + /// Bit: Bit in idle wire. + /// + /// Raises: + /// DAGCircuitError: If the DAG is invalid + fn idle_wires(&self, py: Python, ignore: Option<&Bound>) -> PyResult> { + let mut result: Vec = Vec::new(); + let wires = (0..self.qubit_io_map.len()) + .map(|idx| Wire::Qubit(Qubit(idx as u32))) + .chain((0..self.clbit_io_map.len()).map(|idx| Wire::Clbit(Clbit(idx as u32)))) + .chain(self.var_input_map.keys(py).map(Wire::Var)); + match ignore { + Some(ignore) => { + // Convert the list to a Rust set. + let ignore_set = ignore + .into_iter() + .map(|s| s.extract()) + .collect::>>()?; + for wire in wires { + let nodes_found = self.nodes_on_wire(py, &wire, true).into_iter().any(|node| { + let weight = self.dag.node_weight(node).unwrap(); + if let NodeType::Operation(packed) = weight { + !ignore_set.contains(packed.op.name()) + } else { + false + } + }); + + if !nodes_found { + result.push(match wire { + Wire::Qubit(qubit) => self.qubits.get(qubit).unwrap().clone_ref(py), + Wire::Clbit(clbit) => self.clbits.get(clbit).unwrap().clone_ref(py), + Wire::Var(var) => var, + }); + } + } + } + None => { + for wire in wires { + if self.is_wire_idle(py, &wire)? { + result.push(match wire { + Wire::Qubit(qubit) => self.qubits.get(qubit).unwrap().clone_ref(py), + Wire::Clbit(clbit) => self.clbits.get(clbit).unwrap().clone_ref(py), + Wire::Var(var) => var, + }); + } + } + } + } + Ok(PyTuple::new_bound(py, result).into_any().iter()?.unbind()) + } + + /// Return the number of operations. If there is control flow present, this count may only + /// be an estimate, as the complete control-flow path cannot be statically known. + /// + /// Args: + /// recurse: if ``True``, then recurse into control-flow operations. For loops with + /// known-length iterators are counted unrolled. If-else blocks sum both of the two + /// branches. While loops are counted as if the loop body runs once only. Defaults to + /// ``False`` and raises :class:`.DAGCircuitError` if any control flow is present, to + /// avoid silently returning a mostly meaningless number. + /// + /// Returns: + /// int: the circuit size + /// + /// Raises: + /// DAGCircuitError: if an unknown :class:`.ControlFlowOp` is present in a call with + /// ``recurse=True``, or any control flow is present in a non-recursive call. + #[pyo3(signature= (*, recurse=false))] + fn size(&self, py: Python, recurse: bool) -> PyResult { + let mut length = self.dag.node_count() - (self.width() * 2); + if !recurse { + if CONTROL_FLOW_OP_NAMES + .iter() + .any(|n| self.op_names.contains_key(&n.to_string())) + { + return Err(DAGCircuitError::new_err(concat!( + "Size with control flow is ambiguous.", + " You may use `recurse=True` to get a result", + " but see this method's documentation for the meaning of this." + ))); + } + return Ok(length); + } + + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); + for node_index in + self.op_nodes_by_py_type(imports::CONTROL_FLOW_OP.get_bound(py).downcast()?, true) + { + let NodeType::Operation(node) = &self.dag[node_index] else { + return Err(DAGCircuitError::new_err("unknown control-flow type")); + }; + let OperationRef::Instruction(inst) = node.op.view() else { + unreachable!("Control Flow operations must be a PyInstruction"); + }; + let inst_bound = inst.instruction.bind(py); + if inst_bound.is_instance(imports::FOR_LOOP_OP.get_bound(py))? { + let raw_blocks = inst_bound.getattr("blocks")?; + let blocks: &Bound = raw_blocks.downcast()?; + let block_zero = blocks.get_item(0).unwrap(); + let inner_dag: &DAGCircuit = + &circuit_to_dag.call1((block_zero.clone(),))?.extract()?; + length += node.params_view().len() * inner_dag.size(py, true)? + } else if inst_bound.is_instance(imports::WHILE_LOOP_OP.get_bound(py))? { + let raw_blocks = inst_bound.getattr("blocks")?; + let blocks: &Bound = raw_blocks.downcast()?; + let block_zero = blocks.get_item(0).unwrap(); + let inner_dag: &DAGCircuit = + &circuit_to_dag.call1((block_zero.clone(),))?.extract()?; + length += inner_dag.size(py, true)? + } else if inst_bound.is_instance(imports::IF_ELSE_OP.get_bound(py))? + || inst_bound.is_instance(imports::SWITCH_CASE_OP.get_bound(py))? + { + let raw_blocks = inst_bound.getattr("blocks")?; + let blocks: &Bound = raw_blocks.downcast()?; + for block in blocks.iter() { + let inner_dag: &DAGCircuit = + &circuit_to_dag.call1((block.clone(),))?.extract()?; + length += inner_dag.size(py, true)?; + } + } else { + continue; + } + // We don't count a control-flow node itself! + length -= 1; + } + Ok(length) + } + + /// Return the circuit depth. If there is control flow present, this count may only be an + /// estimate, as the complete control-flow path cannot be statically known. + /// + /// Args: + /// recurse: if ``True``, then recurse into control-flow operations. For loops + /// with known-length iterators are counted as if the loop had been manually unrolled + /// (*i.e.* with each iteration of the loop body written out explicitly). + /// If-else blocks take the longer case of the two branches. While loops are counted as + /// if the loop body runs once only. Defaults to ``False`` and raises + /// :class:`.DAGCircuitError` if any control flow is present, to avoid silently + /// returning a nonsensical number. + /// + /// Returns: + /// int: the circuit depth + /// + /// Raises: + /// DAGCircuitError: if not a directed acyclic graph + /// DAGCircuitError: if unknown control flow is present in a recursive call, or any control + /// flow is present in a non-recursive call. + #[pyo3(signature= (*, recurse=false))] + fn depth(&self, py: Python, recurse: bool) -> PyResult { + if self.qubits.is_empty() && self.clbits.is_empty() && self.vars_info.is_empty() { + return Ok(0); + } + + Ok(if recurse { + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); + let mut node_lookup: HashMap = HashMap::new(); + + for node_index in + self.op_nodes_by_py_type(imports::CONTROL_FLOW_OP.get_bound(py).downcast()?, true) + { + if let NodeType::Operation(node) = &self.dag[node_index] { + if let OperationRef::Instruction(inst) = node.op.view() { + let inst_bound = inst.instruction.bind(py); + let weight = + if inst_bound.is_instance(imports::FOR_LOOP_OP.get_bound(py))? { + node.params_view().len() + } else { + 1 + }; + if weight == 0 { + node_lookup.insert(node_index, 0); + } else { + let raw_blocks = inst_bound.getattr("blocks")?; + let blocks = raw_blocks.downcast::()?; + let mut block_weights: Vec = Vec::with_capacity(blocks.len()); + for block in blocks.iter() { + let inner_dag: &DAGCircuit = + &circuit_to_dag.call1((block,))?.extract()?; + block_weights.push(inner_dag.depth(py, true)?); + } + node_lookup + .insert(node_index, weight * block_weights.iter().max().unwrap()); + } + } + } + } + + let weight_fn = |edge: EdgeReference<'_, Wire>| -> Result { + Ok(*node_lookup.get(&edge.target()).unwrap_or(&1)) + }; + match rustworkx_core::dag_algo::longest_path(&self.dag, weight_fn).unwrap() { + Some(res) => res.1, + None => return Err(DAGCircuitError::new_err("not a DAG")), + } + } else { + if CONTROL_FLOW_OP_NAMES + .iter() + .any(|x| self.op_names.contains_key(&x.to_string())) + { + return Err(DAGCircuitError::new_err("Depth with control flow is ambiguous. You may use `recurse=True` to get a result, but see this method's documentation for the meaning of this.")); + } + + let weight_fn = |_| -> Result { Ok(1) }; + match rustworkx_core::dag_algo::longest_path(&self.dag, weight_fn).unwrap() { + Some(res) => res.1, + None => return Err(DAGCircuitError::new_err("not a DAG")), + } + } - 1) + } + + /// Return the total number of qubits + clbits used by the circuit. + /// This function formerly returned the number of qubits by the calculation + /// return len(self._wires) - self.num_clbits() + /// but was changed by issue #2564 to return number of qubits + clbits + /// with the new function DAGCircuit.num_qubits replacing the former + /// semantic of DAGCircuit.width(). + fn width(&self) -> usize { + self.qubits.len() + self.clbits.len() + self.vars_info.len() + } + + /// Return the total number of qubits used by the circuit. + /// num_qubits() replaces former use of width(). + /// DAGCircuit.width() now returns qubits + clbits for + /// consistency with Circuit.width() [qiskit-terra #2564]. + pub fn num_qubits(&self) -> usize { + self.qubits.len() + } + + /// Return the total number of classical bits used by the circuit. + pub fn num_clbits(&self) -> usize { + self.clbits.len() + } + + /// Compute how many components the circuit can decompose into. + fn num_tensor_factors(&self) -> usize { + // This function was forked from rustworkx's + // number_weekly_connected_components() function as of 0.15.0: + // https://github.com/Qiskit/rustworkx/blob/0.15.0/src/connectivity/mod.rs#L215-L235 + + let mut weak_components = self.dag.node_count(); + let mut vertex_sets = UnionFind::new(self.dag.node_bound()); + for edge in self.dag.edge_references() { + let (a, b) = (edge.source(), edge.target()); + // union the two vertices of the edge + if vertex_sets.union(a.index(), b.index()) { + weak_components -= 1 + }; + } + weak_components + } + + fn __eq__(&self, py: Python, other: &DAGCircuit) -> PyResult { + // Try to convert to float, but in case of unbound ParameterExpressions + // a TypeError will be raise, fallback to normal equality in those + // cases. + let phase_is_close = |self_phase: f64, other_phase: f64| -> bool { + ((self_phase - other_phase + PI).rem_euclid(2. * PI) - PI).abs() <= 1.0e-10 + }; + let normalize_param = |param: &Param| { + if let Param::ParameterExpression(ob) = param { + ob.bind(py) + .call_method0(intern!(py, "numeric")) + .ok() + .map(|ob| ob.extract::()) + .unwrap_or_else(|| Ok(param.clone())) + } else { + Ok(param.clone()) + } + }; + + let phase_eq = match [ + normalize_param(&self.global_phase)?, + normalize_param(&other.global_phase)?, + ] { + [Param::Float(self_phase), Param::Float(other_phase)] => { + Ok(phase_is_close(self_phase, other_phase)) + } + _ => self.global_phase.eq(py, &other.global_phase), + }?; + if !phase_eq { + return Ok(false); + } + if self.calibrations.len() != other.calibrations.len() { + return Ok(false); + } + + for (k, v1) in &self.calibrations { + match other.calibrations.get(k) { + Some(v2) => { + if !v1.bind(py).eq(v2.bind(py))? { + return Ok(false); + } + } + None => { + return Ok(false); + } + } + } + + // We don't do any semantic equivalence between Var nodes, as things stand; DAGs can only be + // equal in our mind if they use the exact same UUID vars. + for (our_vars, their_vars) in self.vars_by_type.iter().zip(&other.vars_by_type) { + if !our_vars.bind(py).eq(their_vars)? { + return Ok(false); + } + } + + let self_bit_indices = { + let indices = self + .qubits + .bits() + .iter() + .chain(self.clbits.bits()) + .enumerate() + .map(|(idx, bit)| (bit, idx)); + indices.into_py_dict_bound(py) + }; + + let other_bit_indices = { + let indices = other + .qubits + .bits() + .iter() + .chain(other.clbits.bits()) + .enumerate() + .map(|(idx, bit)| (bit, idx)); + indices.into_py_dict_bound(py) + }; + + // Check if qregs are the same. + let self_qregs = self.qregs.bind(py); + let other_qregs = other.qregs.bind(py); + if self_qregs.len() != other_qregs.len() { + return Ok(false); + } + for (regname, self_bits) in self_qregs { + let self_bits = self_bits + .getattr("_bits")? + .downcast_into_exact::()?; + let other_bits = match other_qregs.get_item(regname)? { + Some(bits) => bits.getattr("_bits")?.downcast_into_exact::()?, + None => return Ok(false), + }; + if !self + .qubits + .map_bits(self_bits)? + .eq(other.qubits.map_bits(other_bits)?) + { + return Ok(false); + } + } + + // Check if cregs are the same. + let self_cregs = self.cregs.bind(py); + let other_cregs = other.cregs.bind(py); + if self_cregs.len() != other_cregs.len() { + return Ok(false); + } + + for (regname, self_bits) in self_cregs { + let self_bits = self_bits + .getattr("_bits")? + .downcast_into_exact::()?; + let other_bits = match other_cregs.get_item(regname)? { + Some(bits) => bits.getattr("_bits")?.downcast_into_exact::()?, + None => return Ok(false), + }; + if !self + .clbits + .map_bits(self_bits)? + .eq(other.clbits.map_bits(other_bits)?) + { + return Ok(false); + } + } + + // Check for VF2 isomorphic match. + let legacy_condition_eq = imports::LEGACY_CONDITION_CHECK.get_bound(py); + let condition_op_check = imports::CONDITION_OP_CHECK.get_bound(py); + let switch_case_op_check = imports::SWITCH_CASE_OP_CHECK.get_bound(py); + let for_loop_op_check = imports::FOR_LOOP_OP_CHECK.get_bound(py); + let node_match = |n1: &NodeType, n2: &NodeType| -> PyResult { + match [n1, n2] { + [NodeType::Operation(inst1), NodeType::Operation(inst2)] => { + if inst1.op.name() != inst2.op.name() { + return Ok(false); + } + let check_args = || -> bool { + let node1_qargs = self.qargs_cache.intern(inst1.qubits); + let node2_qargs = other.qargs_cache.intern(inst2.qubits); + let node1_cargs = self.cargs_cache.intern(inst1.clbits); + let node2_cargs = other.cargs_cache.intern(inst2.clbits); + if SEMANTIC_EQ_SYMMETRIC.contains(&inst1.op.name()) { + let node1_qargs = + node1_qargs.iter().copied().collect::>(); + let node2_qargs = + node2_qargs.iter().copied().collect::>(); + let node1_cargs = + node1_cargs.iter().copied().collect::>(); + let node2_cargs = + node2_cargs.iter().copied().collect::>(); + if node1_qargs != node2_qargs || node1_cargs != node2_cargs { + return false; + } + } else if node1_qargs != node2_qargs || node1_cargs != node2_cargs { + return false; + } + true + }; + let check_conditions = || -> PyResult { + if let Some(cond1) = inst1 + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()) + { + if let Some(cond2) = inst2 + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()) + { + legacy_condition_eq + .call1((cond1, cond2, &self_bit_indices, &other_bit_indices))? + .extract::() + } else { + Ok(false) + } + } else { + Ok(inst2 + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()) + .is_none()) + } + }; + + match [inst1.op.view(), inst2.op.view()] { + [OperationRef::Standard(_op1), OperationRef::Standard(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? + && check_args() + && check_conditions()? + && inst1 + .params_view() + .iter() + .zip(inst2.params_view().iter()) + .all(|(a, b)| a.is_close(py, b, 1e-10).unwrap())) + } + [OperationRef::Instruction(op1), OperationRef::Instruction(op2)] => { + if op1.control_flow() && op2.control_flow() { + let n1 = self.unpack_into(py, NodeIndex::new(0), n1)?; + let n2 = other.unpack_into(py, NodeIndex::new(0), n2)?; + let name = op1.name(); + if name == "if_else" || name == "while_loop" { + condition_op_check + .call1((n1, n2, &self_bit_indices, &other_bit_indices))? + .extract() + } else if name == "switch_case" { + switch_case_op_check + .call1((n1, n2, &self_bit_indices, &other_bit_indices))? + .extract() + } else if name == "for_loop" { + for_loop_op_check + .call1((n1, n2, &self_bit_indices, &other_bit_indices))? + .extract() + } else { + Err(PyRuntimeError::new_err(format!( + "unhandled control-flow operation: {}", + name + ))) + } + } else { + Ok(inst1.py_op_eq(py, inst2)? + && check_args() + && check_conditions()?) + } + } + [OperationRef::Gate(_op1), OperationRef::Gate(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? && check_args() && check_conditions()?) + } + [OperationRef::Operation(_op1), OperationRef::Operation(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? && check_args()) + } + // Handle the case we end up with a pygate for a standardgate + // this typically only happens if it's a ControlledGate in python + // and we have mutable state set. + [OperationRef::Standard(_op1), OperationRef::Gate(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? && check_args() && check_conditions()?) + } + [OperationRef::Gate(_op1), OperationRef::Standard(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? && check_args() && check_conditions()?) + } + _ => Ok(false), + } + } + [NodeType::QubitIn(bit1), NodeType::QubitIn(bit2)] => Ok(bit1 == bit2), + [NodeType::ClbitIn(bit1), NodeType::ClbitIn(bit2)] => Ok(bit1 == bit2), + [NodeType::QubitOut(bit1), NodeType::QubitOut(bit2)] => Ok(bit1 == bit2), + [NodeType::ClbitOut(bit1), NodeType::ClbitOut(bit2)] => Ok(bit1 == bit2), + [NodeType::VarIn(var1), NodeType::VarIn(var2)] => var1.bind(py).eq(var2), + [NodeType::VarOut(var1), NodeType::VarOut(var2)] => var1.bind(py).eq(var2), + _ => Ok(false), + } + }; + + isomorphism::vf2::is_isomorphic( + &self.dag, + &other.dag, + node_match, + isomorphism::vf2::NoSemanticMatch, + true, + Ordering::Equal, + true, + None, + ) + .map_err(|e| match e { + isomorphism::vf2::IsIsomorphicError::NodeMatcherErr(e) => e, + _ => { + unreachable!() + } + }) + } + + /// Yield nodes in topological order. + /// + /// Args: + /// key (Callable): A callable which will take a DAGNode object and + /// return a string sort key. If not specified the + /// :attr:`~qiskit.dagcircuit.DAGNode.sort_key` attribute will be + /// used as the sort key for each node. + /// + /// Returns: + /// generator(DAGOpNode, DAGInNode, or DAGOutNode): node in topological order + #[pyo3(name = "topological_nodes")] + fn py_topological_nodes( + &self, + py: Python, + key: Option>, + ) -> PyResult> { + let nodes: PyResult> = if let Some(key) = key { + self.topological_key_sort(py, &key)? + .map(|node| self.get_node(py, node)) + .collect() + } else { + // Good path, using interner IDs. + self.topological_nodes()? + .map(|n| self.get_node(py, n)) + .collect() + }; + + Ok(PyTuple::new_bound(py, nodes?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Yield op nodes in topological order. + /// + /// Allowed to pass in specific key to break ties in top order + /// + /// Args: + /// key (Callable): A callable which will take a DAGNode object and + /// return a string sort key. If not specified the + /// :attr:`~qiskit.dagcircuit.DAGNode.sort_key` attribute will be + /// used as the sort key for each node. + /// + /// Returns: + /// generator(DAGOpNode): op node in topological order + #[pyo3(name = "topological_op_nodes")] + fn py_topological_op_nodes( + &self, + py: Python, + key: Option>, + ) -> PyResult> { + let nodes: PyResult> = if let Some(key) = key { + self.topological_key_sort(py, &key)? + .filter_map(|node| match self.dag.node_weight(node) { + Some(NodeType::Operation(_)) => Some(self.get_node(py, node)), + _ => None, + }) + .collect() + } else { + // Good path, using interner IDs. + self.topological_op_nodes()? + .map(|n| self.get_node(py, n)) + .collect() + }; + + Ok(PyTuple::new_bound(py, nodes?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Replace a block of nodes with a single node. + /// + /// This is used to consolidate a block of DAGOpNodes into a single + /// operation. A typical example is a block of gates being consolidated + /// into a single ``UnitaryGate`` representing the unitary matrix of the + /// block. + /// + /// Args: + /// node_block (List[DAGNode]): A list of dag nodes that represents the + /// node block to be replaced + /// op (qiskit.circuit.Operation): The operation to replace the + /// block with + /// wire_pos_map (Dict[Bit, int]): The dictionary mapping the bits to their positions in the + /// output ``qargs`` or ``cargs``. This is necessary to reconstruct the arg order over + /// multiple gates in the combined single op node. If a :class:`.Bit` is not in the + /// dictionary, it will not be added to the args; this can be useful when dealing with + /// control-flow operations that have inherent bits in their ``condition`` or ``target`` + /// fields. + /// cycle_check (bool): When set to True this method will check that + /// replacing the provided ``node_block`` with a single node + /// would introduce a cycle (which would invalidate the + /// ``DAGCircuit``) and will raise a ``DAGCircuitError`` if a cycle + /// would be introduced. This checking comes with a run time + /// penalty. If you can guarantee that your input ``node_block`` is + /// a contiguous block and won't introduce a cycle when it's + /// contracted to a single node, this can be set to ``False`` to + /// improve the runtime performance of this method. + /// + /// Raises: + /// DAGCircuitError: if ``cycle_check`` is set to ``True`` and replacing + /// the specified block introduces a cycle or if ``node_block`` is + /// empty. + /// + /// Returns: + /// DAGOpNode: The op node that replaces the block. + #[pyo3(signature = (node_block, op, wire_pos_map, cycle_check=true))] + fn replace_block_with_op( + &mut self, + py: Python, + node_block: Vec>, + op: Bound, + wire_pos_map: &Bound, + cycle_check: bool, + ) -> PyResult> { + // If node block is empty return early + if node_block.is_empty() { + return Err(DAGCircuitError::new_err( + "Can't replace an empty 'node_block'", + )); + } + + let mut qubit_pos_map: HashMap = HashMap::new(); + let mut clbit_pos_map: HashMap = HashMap::new(); + for (bit, index) in wire_pos_map.iter() { + if bit.is_instance(imports::QUBIT.get_bound(py))? { + qubit_pos_map.insert(self.qubits.find(&bit).unwrap(), index.extract()?); + } else if bit.is_instance(imports::CLBIT.get_bound(py))? { + clbit_pos_map.insert(self.clbits.find(&bit).unwrap(), index.extract()?); + } else { + return Err(DAGCircuitError::new_err( + "Wire map keys must be Qubit or Clbit instances.", + )); + } + } + + let block_ids: Vec<_> = node_block.iter().map(|n| n.node.unwrap()).collect(); + + let mut block_op_names = Vec::new(); + let mut block_qargs: HashSet = HashSet::new(); + let mut block_cargs: HashSet = HashSet::new(); + for nd in &block_ids { + let weight = self.dag.node_weight(*nd); + match weight { + Some(NodeType::Operation(packed)) => { + block_op_names.push(packed.op.name().to_string()); + block_qargs.extend(self.qargs_cache.intern(packed.qubits)); + block_cargs.extend(self.cargs_cache.intern(packed.clbits)); + + if let Some(condition) = packed.condition() { + block_cargs.extend( + self.clbits.map_bits( + self.control_flow_module + .condition_resources(condition.bind(py))? + .clbits + .bind(py), + )?, + ); + continue; + } + + // Add classical bits from SwitchCaseOp, if applicable. + if let OperationRef::Instruction(op) = packed.op.view() { + if op.name() == "switch_case" { + let op_bound = op.instruction.bind(py); + let target = op_bound.getattr(intern!(py, "target"))?; + if target.is_instance(imports::CLBIT.get_bound(py))? { + block_cargs.insert(self.clbits.find(&target).unwrap()); + } else if target + .is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? + { + block_cargs.extend( + self.clbits + .map_bits(target.extract::>>()?)?, + ); + } else { + block_cargs.extend( + self.clbits.map_bits( + self.control_flow_module + .node_resources(&target)? + .clbits + .bind(py), + )?, + ); + } + } + } + } + Some(_) => { + return Err(DAGCircuitError::new_err( + "Nodes in 'node_block' must be of type 'DAGOpNode'.", + )) + } + None => { + return Err(DAGCircuitError::new_err( + "Node in 'node_block' not found in DAG.", + )) + } + } + } + + let mut block_qargs: Vec = block_qargs + .into_iter() + .filter(|q| qubit_pos_map.contains_key(q)) + .collect(); + block_qargs.sort_by_key(|q| qubit_pos_map[q]); + + let mut block_cargs: Vec = block_cargs + .into_iter() + .filter(|c| clbit_pos_map.contains_key(c)) + .collect(); + block_cargs.sort_by_key(|c| clbit_pos_map[c]); + + let py_op = op.extract::()?; + + if py_op.operation.num_qubits() as usize != block_qargs.len() { + return Err(DAGCircuitError::new_err(format!( + "Number of qubits in the replacement operation ({}) is not equal to the number of qubits in the block ({})!", py_op.operation.num_qubits(), block_qargs.len() + ))); + } + + let op_name = py_op.operation.name().to_string(); + let qubits = Interner::intern(&mut self.qargs_cache, block_qargs)?; + let clbits = Interner::intern(&mut self.cargs_cache, block_cargs)?; + let weight = NodeType::Operation(PackedInstruction { + op: py_op.operation, + qubits, + clbits, + params: (!py_op.params.is_empty()).then(|| Box::new(py_op.params)), + extra_attrs: py_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: op.unbind().into(), + }); + + let new_node = self + .dag + .contract_nodes(block_ids, weight, cycle_check) + .map_err(|e| match e { + ContractError::DAGWouldCycle => DAGCircuitError::new_err( + "Replacing the specified node block would introduce a cycle", + ), + })?; + + self.increment_op(op_name.as_str()); + for name in block_op_names { + self.decrement_op(name.as_str()); + } + + self.get_node(py, new_node) + } + + /// Replace one node with dag. + /// + /// Args: + /// node (DAGOpNode): node to substitute + /// input_dag (DAGCircuit): circuit that will substitute the node + /// wires (list[Bit] | Dict[Bit, Bit]): gives an order for (qu)bits + /// in the input circuit. If a list, then the bits refer to those in the ``input_dag``, + /// and the order gets matched to the node wires by qargs first, then cargs, then + /// conditions. If a dictionary, then a mapping of bits in the ``input_dag`` to those + /// that the ``node`` acts on. + /// propagate_condition (bool): If ``True`` (default), then any ``condition`` attribute on + /// the operation within ``node`` is propagated to each node in the ``input_dag``. If + /// ``False``, then the ``input_dag`` is assumed to faithfully implement suitable + /// conditional logic already. This is ignored for :class:`.ControlFlowOp`\\ s (i.e. + /// treated as if it is ``False``); replacements of those must already fulfill the same + /// conditional logic or this function would be close to useless for them. + /// + /// Returns: + /// dict: maps node IDs from `input_dag` to their new node incarnations in `self`. + /// + /// Raises: + /// DAGCircuitError: if met with unexpected predecessor/successors + #[pyo3(signature = (node, input_dag, wires=None, propagate_condition=true))] + fn substitute_node_with_dag( + &mut self, + py: Python, + node: &Bound, + input_dag: &DAGCircuit, + wires: Option>, + propagate_condition: bool, + ) -> PyResult> { + let (node_index, bound_node) = match node.downcast::() { + Ok(bound_node) => (bound_node.borrow().as_ref().node.unwrap(), bound_node), + Err(_) => return Err(DAGCircuitError::new_err("expected node DAGOpNode")), + }; + + let node = match &self.dag[node_index] { + NodeType::Operation(op) => op.clone(), + _ => return Err(DAGCircuitError::new_err("expected node")), + }; + + type WireMapsTuple = (HashMap, HashMap, Py); + + let build_wire_map = |wires: &Bound| -> PyResult { + let qargs_list = imports::BUILTIN_LIST + .get_bound(py) + .call1((bound_node.borrow().get_qargs(py),))?; + let qargs_list = qargs_list.downcast::().unwrap(); + let cargs_list = imports::BUILTIN_LIST + .get_bound(py) + .call1((bound_node.borrow().get_cargs(py),))?; + let cargs_list = cargs_list.downcast::().unwrap(); + let cargs_set = imports::BUILTIN_SET.get_bound(py).call1((cargs_list,))?; + let cargs_set = cargs_set.downcast::().unwrap(); + if !propagate_condition && self.may_have_additional_wires(py, &node) { + let (add_cargs, _add_vars) = + self.additional_wires(py, node.op.view(), node.condition())?; + for wire in add_cargs.iter() { + let clbit = &self.clbits.get(*wire).unwrap(); + if !cargs_set.contains(clbit.clone_ref(py))? { + cargs_list.append(clbit)?; + } + } + } + let qargs_len = qargs_list.len(); + let cargs_len = cargs_list.len(); + + if qargs_len + cargs_len != wires.len() { + return Err(DAGCircuitError::new_err(format!( + "bit mapping invalid: expected {}, got {}", + qargs_len + cargs_len, + wires.len() + ))); + } + let mut qubit_wire_map = HashMap::new(); + let mut clbit_wire_map = HashMap::new(); + let var_map = PyDict::new_bound(py); + for (index, wire) in wires.iter().enumerate() { + if wire.is_instance(imports::QUBIT.get_bound(py))? { + if index >= qargs_len { + unreachable!() + } + let input_qubit: Qubit = input_dag.qubits.find(&wire).unwrap(); + let self_qubit: Qubit = self.qubits.find(&qargs_list.get_item(index)?).unwrap(); + qubit_wire_map.insert(input_qubit, self_qubit); + } else if wire.is_instance(imports::CLBIT.get_bound(py))? { + if index < qargs_len { + unreachable!() + } + clbit_wire_map.insert( + input_dag.clbits.find(&wire).unwrap(), + self.clbits + .find(&cargs_list.get_item(index - qargs_len)?) + .unwrap(), + ); + } else { + return Err(DAGCircuitError::new_err( + "`Var` nodes cannot be remapped during substitution", + )); + } + } + Ok((qubit_wire_map, clbit_wire_map, var_map.unbind())) + }; + + let (mut qubit_wire_map, mut clbit_wire_map, var_map): ( + HashMap, + HashMap, + Py, + ) = match wires { + Some(wires) => match wires.downcast::() { + Ok(bound_wires) => { + let mut qubit_wire_map = HashMap::new(); + let mut clbit_wire_map = HashMap::new(); + let var_map = PyDict::new_bound(py); + for (source_wire, target_wire) in bound_wires.iter() { + if source_wire.is_instance(imports::QUBIT.get_bound(py))? { + qubit_wire_map.insert( + input_dag.qubits.find(&source_wire).unwrap(), + self.qubits.find(&target_wire).unwrap(), + ); + } else if source_wire.is_instance(imports::CLBIT.get_bound(py))? { + clbit_wire_map.insert( + input_dag.clbits.find(&source_wire).unwrap(), + self.clbits.find(&target_wire).unwrap(), + ); + } else { + var_map.set_item(source_wire, target_wire)?; + } + } + (qubit_wire_map, clbit_wire_map, var_map.unbind()) + } + Err(_) => { + let wires: Bound = match wires.downcast::() { + Ok(bound_list) => bound_list.clone(), + // If someone passes a sequence instead of an exact list (tuple is + // occasionally used) cast that to a list and then use it. + Err(_) => { + let raw_wires = imports::BUILTIN_LIST.get_bound(py).call1((wires,))?; + raw_wires.extract()? + } + }; + build_wire_map(&wires)? + } + }, + None => { + let raw_wires = input_dag.get_wires(py); + let binding = raw_wires?; + let wires = binding.bind(py); + build_wire_map(wires)? + } + }; + + let var_iter = input_dag.iter_vars(py)?; + let raw_set = imports::BUILTIN_SET.get_bound(py).call1((var_iter,))?; + let input_dag_var_set: &Bound = raw_set.downcast()?; + + let node_vars = if self.may_have_additional_wires(py, &node) { + let (_additional_clbits, additional_vars) = + self.additional_wires(py, node.op.view(), node.condition())?; + let var_set = PySet::new_bound(py, &additional_vars)?; + if input_dag_var_set + .call_method1(intern!(py, "difference"), (var_set.clone(),))? + .is_truthy()? + { + return Err(DAGCircuitError::new_err(format!( + "Cannot replace a node with a DAG with more variables. Variables in node: {:?}. Variables in dag: {:?}", + var_set.str(), input_dag_var_set.str(), + ))); + } + var_set + } else { + PySet::empty_bound(py)? + }; + let bound_var_map = var_map.bind(py); + for var in input_dag_var_set.iter() { + bound_var_map.set_item(var.clone(), var)?; + } + + for contracted_var in node_vars + .call_method1(intern!(py, "difference"), (input_dag_var_set,))? + .downcast::()? + .iter() + { + let pred = self + .dag + .edges_directed(node_index, Incoming) + .find(|edge| { + if let Wire::Var(var) = edge.weight() { + contracted_var.eq(var).unwrap() + } else { + false + } + }) + .unwrap(); + let succ = self + .dag + .edges_directed(node_index, Outgoing) + .find(|edge| { + if let Wire::Var(var) = edge.weight() { + contracted_var.eq(var).unwrap() + } else { + false + } + }) + .unwrap(); + self.dag.add_edge( + pred.source(), + succ.target(), + Wire::Var(contracted_var.unbind()), + ); + } + + let mut new_input_dag: Option = None; + // It doesn't make sense to try and propagate a condition from a control-flow op; a + // replacement for the control-flow op should implement the operation completely. + let node_map = if propagate_condition && !node.op.control_flow() { + // Nested until https://github.com/rust-lang/rust/issues/53667 is fixed in a stable + // release + if let Some(condition) = node + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()) + { + let mut in_dag = input_dag.copy_empty_like(py, "alike")?; + // The remapping of `condition` below is still using the old code that assumes a 2-tuple. + // This is because this remapping code only makes sense in the case of non-control-flow + // operations being replaced. These can only have the 2-tuple conditions, and the + // ability to set a condition at an individual node level will be deprecated and removed + // in favour of the new-style conditional blocks. The extra logic in here to add + // additional wires into the map as necessary would hugely complicate matters if we tried + // to abstract it out into the `VariableMapper` used elsewhere. + let wire_map = PyDict::new_bound(py); + for (source_qubit, target_qubit) in &qubit_wire_map { + wire_map.set_item( + in_dag.qubits.get(*source_qubit).unwrap().clone_ref(py), + self.qubits.get(*target_qubit).unwrap().clone_ref(py), + )? + } + for (source_clbit, target_clbit) in &clbit_wire_map { + wire_map.set_item( + in_dag.clbits.get(*source_clbit).unwrap().clone_ref(py), + self.clbits.get(*target_clbit).unwrap().clone_ref(py), + )? + } + wire_map.update(var_map.bind(py).as_mapping())?; + + let reverse_wire_map = wire_map.iter().map(|(k, v)| (v, k)).into_py_dict_bound(py); + let (py_target, py_value): (Bound, Bound) = + condition.bind(py).extract()?; + let (py_new_target, target_cargs) = + if py_target.is_instance(imports::CLBIT.get_bound(py))? { + let new_target = reverse_wire_map + .get_item(&py_target)? + .map(Ok::<_, PyErr>) + .unwrap_or_else(|| { + // Target was not in node's wires, so we need a dummy. + let new_target = imports::CLBIT.get_bound(py).call0()?; + in_dag.add_clbit_unchecked(py, &new_target)?; + wire_map.set_item(&new_target, &py_target)?; + reverse_wire_map.set_item(&py_target, &new_target)?; + Ok(new_target) + })?; + (new_target.clone(), PySet::new_bound(py, &[new_target])?) + } else { + // ClassicalRegister + let target_bits: Vec> = + py_target.iter()?.collect::>()?; + let mapped_bits: Vec>> = target_bits + .iter() + .map(|b| reverse_wire_map.get_item(b)) + .collect::>()?; + + let mut new_target = Vec::with_capacity(target_bits.len()); + let target_cargs = PySet::empty_bound(py)?; + for (ours, theirs) in target_bits.into_iter().zip(mapped_bits) { + if let Some(theirs) = theirs { + // Target bit was in node's wires. + new_target.push(theirs.clone()); + target_cargs.add(theirs)?; + } else { + // Target bit was not in node's wires, so we need a dummy. + let theirs = imports::CLBIT.get_bound(py).call0()?; + in_dag.add_clbit_unchecked(py, &theirs)?; + wire_map.set_item(&theirs, &ours)?; + reverse_wire_map.set_item(&ours, &theirs)?; + new_target.push(theirs.clone()); + target_cargs.add(theirs)?; + } + } + let kwargs = [("bits", new_target.into_py(py))].into_py_dict_bound(py); + let new_target_register = imports::CLASSICAL_REGISTER + .get_bound(py) + .call((), Some(&kwargs))?; + in_dag.add_creg(py, &new_target_register)?; + (new_target_register, target_cargs) + }; + let new_condition = PyTuple::new_bound(py, [py_new_target, py_value]); + + qubit_wire_map.clear(); + clbit_wire_map.clear(); + for item in wire_map.items().iter() { + let (in_bit, self_bit): (Bound, Bound) = item.extract()?; + if in_bit.is_instance(imports::QUBIT.get_bound(py))? { + let in_index = in_dag.qubits.find(&in_bit).unwrap(); + let self_index = self.qubits.find(&self_bit).unwrap(); + qubit_wire_map.insert(in_index, self_index); + } else { + let in_index = in_dag.clbits.find(&in_bit).unwrap(); + let self_index = self.clbits.find(&self_bit).unwrap(); + clbit_wire_map.insert(in_index, self_index); + } + } + for in_node_index in input_dag.topological_op_nodes()? { + let in_node = &input_dag.dag[in_node_index]; + if let NodeType::Operation(inst) = in_node { + if inst + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()) + .is_some() + { + return Err(DAGCircuitError::new_err( + "cannot propagate a condition to an element that already has one", + )); + } + let cargs = input_dag.cargs_cache.intern(inst.clbits); + let cargs_bits: Vec = input_dag + .clbits + .map_indices(cargs) + .map(|x| x.clone_ref(py)) + .collect(); + if !target_cargs + .call_method1(intern!(py, "intersection"), (cargs_bits,))? + .downcast::()? + .is_empty() + { + return Err(DAGCircuitError::new_err("cannot propagate a condition to an element that acts on those bits")); + } + let mut new_inst = inst.clone(); + if new_condition.is_truthy()? { + if let Some(ref mut attrs) = new_inst.extra_attrs { + attrs.condition = Some(new_condition.as_any().clone().unbind()); + } else { + new_inst.extra_attrs = Some(Box::new(ExtraInstructionAttributes { + condition: Some(new_condition.as_any().clone().unbind()), + label: None, + duration: None, + unit: None, + })); + } + #[cfg(feature = "cache_pygates")] + { + new_inst.py_op.take(); + } + } + in_dag.push_back(py, new_inst)?; + } + } + let node_map = self.substitute_node_with_subgraph( + py, + node_index, + &in_dag, + &qubit_wire_map, + &clbit_wire_map, + &var_map, + )?; + new_input_dag = Some(in_dag); + node_map + } else { + self.substitute_node_with_subgraph( + py, + node_index, + input_dag, + &qubit_wire_map, + &clbit_wire_map, + &var_map, + )? + } + } else { + self.substitute_node_with_subgraph( + py, + node_index, + input_dag, + &qubit_wire_map, + &clbit_wire_map, + &var_map, + )? + }; + self.global_phase = add_global_phase(py, &self.global_phase, &input_dag.global_phase)?; + + let wire_map_dict = PyDict::new_bound(py); + for (source, target) in clbit_wire_map.iter() { + let source_bit = match new_input_dag { + Some(ref in_dag) => in_dag.clbits.get(*source), + None => input_dag.clbits.get(*source), + }; + let target_bit = self.clbits.get(*target); + wire_map_dict.set_item(source_bit, target_bit)?; + } + let bound_var_map = var_map.bind(py); + + // Note: creating this list to hold new registers created by the mapper is a temporary + // measure until qiskit.expr is ported to Rust. It is necessary because we cannot easily + // have Python call back to DAGCircuit::add_creg while we're currently borrowing + // the DAGCircuit. + let new_registers = PyList::empty_bound(py); + let add_new_register = new_registers.getattr("append")?.unbind(); + let flush_new_registers = |dag: &mut DAGCircuit| -> PyResult<()> { + for reg in &new_registers { + dag.add_creg(py, ®)?; + } + new_registers.del_slice(0, new_registers.len())?; + Ok(()) + }; + + let variable_mapper = PyVariableMapper::new( + py, + self.cregs.bind(py).values().into_any(), + Some(wire_map_dict), + Some(bound_var_map.clone()), + Some(add_new_register), + )?; + + for (old_node_index, new_node_index) in node_map.iter() { + let old_node = match new_input_dag { + Some(ref in_dag) => &in_dag.dag[*old_node_index], + None => &input_dag.dag[*old_node_index], + }; + if let NodeType::Operation(old_inst) = old_node { + if let OperationRef::Instruction(old_op) = old_inst.op.view() { + if old_op.name() == "switch_case" { + let raw_target = old_op.instruction.getattr(py, "target")?; + let target = raw_target.bind(py); + let kwargs = PyDict::new_bound(py); + kwargs.set_item( + "label", + old_inst + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.label.as_ref()), + )?; + let new_op = imports::SWITCH_CASE_OP.get_bound(py).call( + ( + variable_mapper.map_target(target)?, + old_op.instruction.call_method0(py, "cases_specifier")?, + ), + Some(&kwargs), + )?; + flush_new_registers(self)?; + + if let NodeType::Operation(ref mut new_inst) = + &mut self.dag[*new_node_index] + { + new_inst.op = PyInstruction { + qubits: old_op.num_qubits(), + clbits: old_op.num_clbits(), + params: old_op.num_params(), + control_flow: old_op.control_flow(), + op_name: old_op.name().to_string(), + instruction: new_op.clone().unbind(), + } + .into(); + #[cfg(feature = "cache_pygates")] + { + new_inst.py_op = new_op.unbind().into(); + } + } + } + } + if let Some(condition) = old_inst + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()) + { + if old_inst.op.name() != "switch_case" { + let new_condition: Option = variable_mapper + .map_condition(condition.bind(py), false)? + .extract()?; + flush_new_registers(self)?; + + if let NodeType::Operation(ref mut new_inst) = + &mut self.dag[*new_node_index] + { + match &mut new_inst.extra_attrs { + Some(attrs) => attrs.condition.clone_from(&new_condition), + None => { + new_inst.extra_attrs = + Some(Box::new(ExtraInstructionAttributes { + label: None, + condition: new_condition.clone(), + unit: None, + duration: None, + })) + } + } + #[cfg(feature = "cache_pygates")] + { + new_inst.py_op.take(); + } + match new_inst.op.view() { + OperationRef::Instruction(py_inst) => { + py_inst + .instruction + .setattr(py, "condition", new_condition)?; + } + OperationRef::Gate(py_gate) => { + py_gate.gate.setattr(py, "condition", new_condition)?; + } + OperationRef::Operation(py_op) => { + py_op.operation.setattr(py, "condition", new_condition)?; + } + OperationRef::Standard(_) => {} + } + } + } + } + } + } + let out_dict = PyDict::new_bound(py); + for (old_index, new_index) in node_map { + out_dict.set_item(old_index.index(), self.get_node(py, new_index)?)?; + } + Ok(out_dict.unbind()) + } + + /// Replace a DAGOpNode with a single operation. qargs, cargs and + /// conditions for the new operation will be inferred from the node to be + /// replaced. The new operation will be checked to match the shape of the + /// replaced operation. + /// + /// Args: + /// node (DAGOpNode): Node to be replaced + /// op (qiskit.circuit.Operation): The :class:`qiskit.circuit.Operation` + /// instance to be added to the DAG + /// inplace (bool): Optional, default False. If True, existing DAG node + /// will be modified to include op. Otherwise, a new DAG node will + /// be used. + /// propagate_condition (bool): Optional, default True. If True, a condition on the + /// ``node`` to be replaced will be applied to the new ``op``. This is the legacy + /// behaviour. If either node is a control-flow operation, this will be ignored. If + /// the ``op`` already has a condition, :exc:`.DAGCircuitError` is raised. + /// + /// Returns: + /// DAGOpNode: the new node containing the added operation. + /// + /// Raises: + /// DAGCircuitError: If replacement operation was incompatible with + /// location of target node. + #[pyo3(signature = (node, op, inplace=false, propagate_condition=true))] + fn substitute_node( + &mut self, + node: &Bound, + op: &Bound, + inplace: bool, + propagate_condition: bool, + ) -> PyResult> { + let mut node: PyRefMut = match node.downcast() { + Ok(node) => node.borrow_mut(), + Err(_) => return Err(DAGCircuitError::new_err("Only DAGOpNodes can be replaced.")), + }; + let py = op.py(); + let node_index = node.as_ref().node.unwrap(); + // Extract information from node that is going to be replaced + let old_packed = match self.dag.node_weight(node_index) { + Some(NodeType::Operation(old_packed)) => old_packed.clone(), + Some(_) => { + return Err(DAGCircuitError::new_err( + "'node' must be of type 'DAGOpNode'.", + )) + } + None => return Err(DAGCircuitError::new_err("'node' not found in DAG.")), + }; + // Extract information from new op + let new_op = op.extract::()?; + let current_wires: HashSet = self + .dag + .edges(node_index) + .map(|e| e.weight().clone()) + .collect(); + let mut new_wires: HashSet = self + .qargs_cache + .intern(old_packed.qubits) + .iter() + .map(|x| Wire::Qubit(*x)) + .chain( + self.cargs_cache + .intern(old_packed.clbits) + .iter() + .map(|x| Wire::Clbit(*x)), + ) + .collect(); + let (additional_clbits, additional_vars) = self.additional_wires( + py, + new_op.operation.view(), + new_op + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()), + )?; + new_wires.extend(additional_clbits.iter().map(|x| Wire::Clbit(*x))); + new_wires.extend(additional_vars.iter().map(|x| Wire::Var(x.clone_ref(py)))); + + if old_packed.op.num_qubits() != new_op.operation.num_qubits() + || old_packed.op.num_clbits() != new_op.operation.num_clbits() + { + return Err(DAGCircuitError::new_err( + format!( + "Cannot replace node of width ({} qubits, {} clbits) with operation of mismatched width ({} qubits, {} clbits)", + old_packed.op.num_qubits(), old_packed.op.num_clbits(), new_op.operation.num_qubits(), new_op.operation.num_clbits() + ))); + } + + #[cfg(feature = "cache_pygates")] + let mut py_op_cache = Some(op.clone().unbind()); + + let mut extra_attrs = new_op.extra_attrs.clone(); + // If either operation is a control-flow operation, propagate_condition is ignored + if propagate_condition + && !(node.instruction.operation.control_flow() || new_op.operation.control_flow()) + { + // if new_op has a condition, the condition can't be propagated from the old node + if new_op + .extra_attrs + .as_ref() + .and_then(|extra| extra.condition.as_ref()) + .is_some() + { + return Err(DAGCircuitError::new_err( + "Cannot propagate a condition to an operation that already has one.", + )); + } + if let Some(old_condition) = old_packed.condition() { + if matches!(new_op.operation.view(), OperationRef::Operation(_)) { + return Err(DAGCircuitError::new_err( + "Cannot add a condition on a generic Operation.", + )); + } + if let Some(ref mut extra) = extra_attrs { + extra.condition = Some(old_condition.clone_ref(py)); + } else { + extra_attrs = ExtraInstructionAttributes::new( + None, + None, + None, + Some(old_condition.clone_ref(py)), + ) + .map(Box::new) + } + let binding = self + .control_flow_module + .condition_resources(old_condition.bind(py))?; + let condition_clbits = binding.clbits.bind(py); + for bit in condition_clbits { + new_wires.insert(Wire::Clbit(self.clbits.find(&bit).unwrap())); + } + let op_ref = new_op.operation.view(); + if let OperationRef::Instruction(inst) = op_ref { + inst.instruction + .bind(py) + .setattr(intern!(py, "condition"), old_condition)?; + } else if let OperationRef::Gate(gate) = op_ref { + gate.gate.bind(py).call_method1( + intern!(py, "c_if"), + old_condition.downcast_bound::(py)?, + )?; + } + #[cfg(feature = "cache_pygates")] + { + py_op_cache = None; + } + } + }; + if new_wires != current_wires { + // The new wires must be a non-strict subset of the current wires; if they add new + // wires, we'd not know where to cut the existing wire to insert the new dependency. + return Err(DAGCircuitError::new_err(format!( + "New operation '{:?}' does not span the same wires as the old node '{:?}'. New wires: {:?}, old_wires: {:?}.", op.str(), old_packed.op.view(), new_wires, current_wires + ))); + } + + if inplace { + node.instruction.operation = new_op.operation.clone(); + node.instruction.params = new_op.params.clone(); + node.instruction.extra_attrs = extra_attrs.clone(); + #[cfg(feature = "cache_pygates")] + { + node.instruction.py_op = py_op_cache + .as_ref() + .map(|ob| OnceCell::from(ob.clone_ref(py))) + .unwrap_or_default(); + } + } + // Clone op data, as it will be moved into the PackedInstruction + let new_weight = NodeType::Operation(PackedInstruction { + op: new_op.operation.clone(), + qubits: old_packed.qubits, + clbits: old_packed.clbits, + params: (!new_op.params.is_empty()).then(|| new_op.params.into()), + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: py_op_cache.map(OnceCell::from).unwrap_or_default(), + }); + let node_index = node.as_ref().node.unwrap(); + if let Some(weight) = self.dag.node_weight_mut(node_index) { + *weight = new_weight; + } + + // Update self.op_names + self.decrement_op(old_packed.op.name()); + self.increment_op(new_op.operation.name()); + + if inplace { + Ok(node.into_py(py)) + } else { + self.get_node(py, node_index) + } + } + + /// Decompose the circuit into sets of qubits with no gates connecting them. + /// + /// Args: + /// remove_idle_qubits (bool): Flag denoting whether to remove idle qubits from + /// the separated circuits. If ``False``, each output circuit will contain the + /// same number of qubits as ``self``. + /// + /// Returns: + /// List[DAGCircuit]: The circuits resulting from separating ``self`` into sets + /// of disconnected qubits + /// + /// Each :class:`~.DAGCircuit` instance returned by this method will contain the same number of + /// clbits as ``self``. The global phase information in ``self`` will not be maintained + /// in the subcircuits returned by this method. + #[pyo3(signature = (remove_idle_qubits=false, *, vars_mode="alike"))] + fn separable_circuits( + &self, + py: Python, + remove_idle_qubits: bool, + vars_mode: &str, + ) -> PyResult> { + let connected_components = rustworkx_core::connectivity::connected_components(&self.dag); + let dags = PyList::empty_bound(py); + + for comp_nodes in connected_components.iter() { + let mut new_dag = self.copy_empty_like(py, vars_mode)?; + new_dag.global_phase = Param::Float(0.); + + // A map from nodes in the this DAGCircuit to nodes in the new dag. Used for adding edges + let mut node_map: HashMap = + HashMap::with_capacity(comp_nodes.len()); + + // Adding the nodes to the new dag + let mut non_classical = false; + for node in comp_nodes { + match self.dag.node_weight(*node) { + Some(w) => match w { + NodeType::ClbitIn(b) => { + let clbit_in = new_dag.clbit_io_map[b.0 as usize][0]; + node_map.insert(*node, clbit_in); + } + NodeType::ClbitOut(b) => { + let clbit_out = new_dag.clbit_io_map[b.0 as usize][1]; + node_map.insert(*node, clbit_out); + } + NodeType::QubitIn(q) => { + let qbit_in = new_dag.qubit_io_map[q.0 as usize][0]; + node_map.insert(*node, qbit_in); + non_classical = true; + } + NodeType::QubitOut(q) => { + let qbit_out = new_dag.qubit_io_map[q.0 as usize][1]; + node_map.insert(*node, qbit_out); + non_classical = true; + } + NodeType::VarIn(v) => { + let var_in = new_dag.var_input_map.get(py, v).unwrap(); + node_map.insert(*node, var_in); + } + NodeType::VarOut(v) => { + let var_out = new_dag.var_output_map.get(py, v).unwrap(); + node_map.insert(*node, var_out); + } + NodeType::Operation(pi) => { + let new_node = new_dag.dag.add_node(NodeType::Operation(pi.clone())); + new_dag.increment_op(pi.op.name()); + node_map.insert(*node, new_node); + non_classical = true; + } + }, + None => panic!("DAG node without payload!"), + } + } + if !non_classical { + continue; + } + let node_filter = |node: NodeIndex| -> bool { node_map.contains_key(&node) }; + + let filtered = NodeFiltered(&self.dag, node_filter); + + // Remove the edges added by copy_empty_like (as idle wires) to avoid duplication + new_dag.dag.clear_edges(); + for edge in filtered.edge_references() { + let new_source = node_map[&edge.source()]; + let new_target = node_map[&edge.target()]; + new_dag + .dag + .add_edge(new_source, new_target, edge.weight().clone()); + } + // Add back any edges for idle wires + for (qubit, [in_node, out_node]) in new_dag + .qubit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Qubit(idx as u32), indices)) + { + if new_dag.dag.edges(*in_node).next().is_none() { + new_dag + .dag + .add_edge(*in_node, *out_node, Wire::Qubit(qubit)); + } + } + for (clbit, [in_node, out_node]) in new_dag + .clbit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Clbit(idx as u32), indices)) + { + if new_dag.dag.edges(*in_node).next().is_none() { + new_dag + .dag + .add_edge(*in_node, *out_node, Wire::Clbit(clbit)); + } + } + for (var, in_node) in new_dag.var_input_map.iter(py) { + if new_dag.dag.edges(in_node).next().is_none() { + let out_node = new_dag.var_output_map.get(py, &var).unwrap(); + new_dag + .dag + .add_edge(in_node, out_node, Wire::Var(var.clone_ref(py))); + } + } + if remove_idle_qubits { + let idle_wires: Vec> = new_dag + .idle_wires(py, None)? + .into_bound(py) + .map(|q| q.unwrap()) + .filter(|e| e.is_instance(imports::QUBIT.get_bound(py)).unwrap()) + .collect(); + + let qubits = PyTuple::new_bound(py, idle_wires); + new_dag.remove_qubits(py, &qubits)?; // TODO: this does not really work, some issue with remove_qubits itself + } + + dags.append(pyo3::Py::new(py, new_dag)?)?; + } + + Ok(dags.unbind()) + } + + /// Swap connected nodes e.g. due to commutation. + /// + /// Args: + /// node1 (OpNode): predecessor node + /// node2 (OpNode): successor node + /// + /// Raises: + /// DAGCircuitError: if either node is not an OpNode or nodes are not connected + fn swap_nodes(&mut self, node1: &DAGNode, node2: &DAGNode) -> PyResult<()> { + let node1 = node1.node.unwrap(); + let node2 = node2.node.unwrap(); + + // Check that both nodes correspond to operations + if !matches!(self.dag.node_weight(node1).unwrap(), NodeType::Operation(_)) + || !matches!(self.dag.node_weight(node2).unwrap(), NodeType::Operation(_)) + { + return Err(DAGCircuitError::new_err( + "Nodes to swap are not both DAGOpNodes", + )); + } + + // Gather all wires connecting node1 and node2. + // This functionality was extracted from rustworkx's 'get_edge_data' + let wires: Vec = self + .dag + .edges(node1) + .filter(|edge| edge.target() == node2) + .map(|edge| edge.weight().clone()) + .collect(); + + if wires.is_empty() { + return Err(DAGCircuitError::new_err( + "Attempt to swap unconnected nodes", + )); + }; + + // Closure that finds the first parent/child node connected to a reference node by given wire + // and returns relevant edge information depending on the specified direction: + // - Incoming -> parent -> outputs (parent_edge_id, parent_source_node_id) + // - Outgoing -> child -> outputs (child_edge_id, child_target_node_id) + // This functionality was inspired in rustworkx's 'find_predecessors_by_edge' and 'find_successors_by_edge'. + let directed_edge_for_wire = |node: NodeIndex, direction: Direction, wire: &Wire| { + for edge in self.dag.edges_directed(node, direction) { + if wire == edge.weight() { + match direction { + Incoming => return Some((edge.id(), edge.source())), + Outgoing => return Some((edge.id(), edge.target())), + } + } + } + None + }; + + // Vector that contains a tuple of (wire, edge_info, parent_info, child_info) per wire in wires + let relevant_edges = wires + .iter() + .rev() + .map(|wire| { + ( + wire, + directed_edge_for_wire(node1, Outgoing, wire).unwrap(), + directed_edge_for_wire(node1, Incoming, wire).unwrap(), + directed_edge_for_wire(node2, Outgoing, wire).unwrap(), + ) + }) + .collect::>(); + + // Iterate over relevant edges and modify self.dag + for (wire, (node1_to_node2, _), (parent_to_node1, parent), (node2_to_child, child)) in + relevant_edges + { + self.dag.remove_edge(parent_to_node1); + self.dag.add_edge(parent, node2, wire.clone()); + self.dag.remove_edge(node1_to_node2); + self.dag.add_edge(node2, node1, wire.clone()); + self.dag.remove_edge(node2_to_child); + self.dag.add_edge(node1, child, wire.clone()); + } + Ok(()) + } + + /// Get the node in the dag. + /// + /// Args: + /// node_id(int): Node identifier. + /// + /// Returns: + /// node: the node. + fn node(&self, py: Python, node_id: isize) -> PyResult> { + self.get_node(py, NodeIndex::new(node_id as usize)) + } + + /// Iterator for node values. + /// + /// Yield: + /// node: the node. + fn nodes(&self, py: Python) -> PyResult> { + let result: PyResult> = self + .dag + .node_references() + .map(|(node, weight)| self.unpack_into(py, node, weight)) + .collect(); + let tup = PyTuple::new_bound(py, result?); + Ok(tup.into_any().iter().unwrap().unbind()) + } + + /// Iterator for edge values with source and destination node. + /// + /// This works by returning the outgoing edges from the specified nodes. If + /// no nodes are specified all edges from the graph are returned. + /// + /// Args: + /// nodes(DAGOpNode, DAGInNode, or DAGOutNode|list(DAGOpNode, DAGInNode, or DAGOutNode): + /// Either a list of nodes or a single input node. If none is specified, + /// all edges are returned from the graph. + /// + /// Yield: + /// edge: the edge as a tuple with the format + /// (source node, destination node, edge wire) + fn edges(&self, nodes: Option>, py: Python) -> PyResult> { + let get_node_index = |obj: &Bound| -> PyResult { + Ok(obj.downcast::()?.borrow().node.unwrap()) + }; + + let actual_nodes: Vec<_> = match nodes { + None => self.dag.node_indices().collect(), + Some(nodes) => { + let mut out = Vec::new(); + if let Ok(node) = get_node_index(&nodes) { + out.push(node); + } else { + for node in nodes.iter()? { + out.push(get_node_index(&node?)?); + } + } + out + } + }; + + let mut edges = Vec::new(); + for node in actual_nodes { + for edge in self.dag.edges_directed(node, Outgoing) { + edges.push(( + self.get_node(py, edge.source())?, + self.get_node(py, edge.target())?, + match edge.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }, + )) + } + } + + Ok(PyTuple::new_bound(py, edges) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Get the list of "op" nodes in the dag. + /// + /// Args: + /// op (Type): :class:`qiskit.circuit.Operation` subclass op nodes to + /// return. If None, return all op nodes. + /// include_directives (bool): include `barrier`, `snapshot` etc. + /// + /// Returns: + /// list[DAGOpNode]: the list of node ids containing the given op. + #[pyo3(name= "op_nodes", signature=(op=None, include_directives=true))] + fn py_op_nodes( + &self, + py: Python, + op: Option<&Bound>, + include_directives: bool, + ) -> PyResult>> { + let mut nodes = Vec::new(); + let filter_is_nonstandard = if let Some(op) = op { + op.getattr(intern!(py, "_standard_gate")).ok().is_none() + } else { + true + }; + for (node, weight) in self.dag.node_references() { + if let NodeType::Operation(packed) = &weight { + if !include_directives && packed.op.directive() { + continue; + } + if let Some(op_type) = op { + // This middle catch is to avoid Python-space operation creation for most uses of + // `op`; we're usually just looking for control-flow ops, and standard gates + // aren't control-flow ops. + if !(filter_is_nonstandard && packed.op.try_standard_gate().is_some()) + && packed.op.py_op_is_instance(op_type)? + { + nodes.push(self.unpack_into(py, node, weight)?); + } + } else { + nodes.push(self.unpack_into(py, node, weight)?); + } + } + } + Ok(nodes) + } + + /// Get the list of gate nodes in the dag. + /// + /// Returns: + /// list[DAGOpNode]: the list of DAGOpNodes that represent gates. + fn gate_nodes(&self, py: Python) -> PyResult>> { + self.dag + .node_references() + .filter_map(|(node, weight)| match weight { + NodeType::Operation(ref packed) => match packed.op.view() { + OperationRef::Gate(_) | OperationRef::Standard(_) => { + Some(self.unpack_into(py, node, weight)) + } + _ => None, + }, + _ => None, + }) + .collect() + } + + /// Get the set of "op" nodes with the given name. + #[pyo3(signature = (*names))] + fn named_nodes(&self, py: Python<'_>, names: &Bound) -> PyResult>> { + let mut names_set: HashSet = HashSet::with_capacity(names.len()); + for name_obj in names.iter() { + names_set.insert(name_obj.extract::()?); + } + let mut result: Vec> = Vec::new(); + for (id, weight) in self.dag.node_references() { + if let NodeType::Operation(ref packed) = weight { + if names_set.contains(packed.op.name()) { + result.push(self.unpack_into(py, id, weight)?); + } + } + } + Ok(result) + } + + /// Get list of 2 qubit operations. Ignore directives like snapshot and barrier. + fn two_qubit_ops(&self, py: Python) -> PyResult>> { + let mut nodes = Vec::new(); + for (node, weight) in self.dag.node_references() { + if let NodeType::Operation(ref packed) = weight { + if packed.op.directive() { + continue; + } + + let qargs = self.qargs_cache.intern(packed.qubits); + if qargs.len() == 2 { + nodes.push(self.unpack_into(py, node, weight)?); + } + } + } + Ok(nodes) + } + + /// Get list of 3+ qubit operations. Ignore directives like snapshot and barrier. + fn multi_qubit_ops(&self, py: Python) -> PyResult>> { + let mut nodes = Vec::new(); + for (node, weight) in self.dag.node_references() { + if let NodeType::Operation(ref packed) = weight { + if packed.op.directive() { + continue; + } + + let qargs = self.qargs_cache.intern(packed.qubits); + if qargs.len() >= 3 { + nodes.push(self.unpack_into(py, node, weight)?); + } + } + } + Ok(nodes) + } + + /// Returns the longest path in the dag as a list of DAGOpNodes, DAGInNodes, and DAGOutNodes. + fn longest_path(&self, py: Python) -> PyResult> { + let weight_fn = |_| -> Result { Ok(1) }; + match rustworkx_core::dag_algo::longest_path(&self.dag, weight_fn).unwrap() { + Some(res) => res.0, + None => return Err(DAGCircuitError::new_err("not a DAG")), + } + .into_iter() + .map(|node_index| self.get_node(py, node_index)) + .collect() + } + + /// Returns iterator of the successors of a node as DAGOpNodes and DAGOutNodes.""" + fn successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let successors: PyResult> = self + .dag + .neighbors_directed(node.node.unwrap(), Outgoing) + .unique() + .map(|i| self.get_node(py, i)) + .collect(); + Ok(PyTuple::new_bound(py, successors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns iterator of the predecessors of a node as DAGOpNodes and DAGInNodes. + fn predecessors(&self, py: Python, node: &DAGNode) -> PyResult> { + let predecessors: PyResult> = self + .dag + .neighbors_directed(node.node.unwrap(), Incoming) + .unique() + .map(|i| self.get_node(py, i)) + .collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns iterator of "op" successors of a node in the dag. + fn op_successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let predecessors: PyResult> = self + .dag + .neighbors_directed(node.node.unwrap(), Outgoing) + .unique() + .filter_map(|i| match self.dag[i] { + NodeType::Operation(_) => Some(self.get_node(py, i)), + _ => None, + }) + .collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns the iterator of "op" predecessors of a node in the dag. + fn op_predecessors(&self, py: Python, node: &DAGNode) -> PyResult> { + let predecessors: PyResult> = self + .dag + .neighbors_directed(node.node.unwrap(), Incoming) + .unique() + .filter_map(|i| match self.dag[i] { + NodeType::Operation(_) => Some(self.get_node(py, i)), + _ => None, + }) + .collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Checks if a second node is in the successors of node. + fn is_successor(&self, node: &DAGNode, node_succ: &DAGNode) -> bool { + self.dag + .find_edge(node.node.unwrap(), node_succ.node.unwrap()) + .is_some() + } + + /// Checks if a second node is in the predecessors of node. + fn is_predecessor(&self, node: &DAGNode, node_pred: &DAGNode) -> bool { + self.dag + .find_edge(node_pred.node.unwrap(), node.node.unwrap()) + .is_some() + } + + /// Returns iterator of the predecessors of a node that are + /// connected by a quantum edge as DAGOpNodes and DAGInNodes. + #[pyo3(name = "quantum_predecessors")] + fn py_quantum_predecessors(&self, py: Python, node: &DAGNode) -> PyResult> { + let predecessors: PyResult> = self + .quantum_predecessors(node.node.unwrap()) + .map(|i| self.get_node(py, i)) + .collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns iterator of the successors of a node that are + /// connected by a quantum edge as DAGOpNodes and DAGOutNodes. + #[pyo3(name = "quantum_successors")] + fn py_quantum_successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let successors: PyResult> = self + .quantum_successors(node.node.unwrap()) + .map(|i| self.get_node(py, i)) + .collect(); + Ok(PyTuple::new_bound(py, successors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns iterator of the predecessors of a node that are + /// connected by a classical edge as DAGOpNodes and DAGInNodes. + fn classical_predecessors(&self, py: Python, node: &DAGNode) -> PyResult> { + let edges = self.dag.edges_directed(node.node.unwrap(), Incoming); + let filtered = edges.filter_map(|e| match e.weight() { + Wire::Qubit(_) => None, + _ => Some(e.source()), + }); + let predecessors: PyResult> = + filtered.unique().map(|i| self.get_node(py, i)).collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns set of the ancestors of a node as DAGOpNodes and DAGInNodes. + #[pyo3(name = "ancestors")] + fn py_ancestors(&self, py: Python, node: &DAGNode) -> PyResult> { + let ancestors: PyResult> = self + .ancestors(node.node.unwrap()) + .map(|node| self.get_node(py, node)) + .collect(); + Ok(PySet::new_bound(py, &ancestors?)?.unbind()) + } + + /// Returns set of the descendants of a node as DAGOpNodes and DAGOutNodes. + #[pyo3(name = "descendants")] + fn py_descendants(&self, py: Python, node: &DAGNode) -> PyResult> { + let descendants: PyResult> = self + .descendants(node.node.unwrap()) + .map(|node| self.get_node(py, node)) + .collect(); + Ok(PySet::new_bound(py, &descendants?)?.unbind()) + } + + /// Returns an iterator of tuples of (DAGNode, [DAGNodes]) where the DAGNode is the current node + /// and [DAGNode] is its successors in BFS order. + #[pyo3(name = "bfs_successors")] + fn py_bfs_successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let successor_index: PyResult)>> = self + .bfs_successors(node.node.unwrap()) + .map(|(node, nodes)| -> PyResult<(PyObject, Vec)> { + Ok(( + self.get_node(py, node)?, + nodes + .iter() + .map(|sub_node| self.get_node(py, *sub_node)) + .collect::>>()?, + )) + }) + .collect(); + Ok(PyList::new_bound(py, successor_index?) + .into_any() + .iter()? + .unbind()) + } + + /// Returns iterator of the successors of a node that are + /// connected by a classical edge as DAGOpNodes and DAGOutNodes. + fn classical_successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let edges = self.dag.edges_directed(node.node.unwrap(), Outgoing); + let filtered = edges.filter_map(|e| match e.weight() { + Wire::Qubit(_) => None, + _ => Some(e.target()), + }); + let predecessors: PyResult> = + filtered.unique().map(|i| self.get_node(py, i)).collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Remove an operation node n. + /// + /// Add edges from predecessors to successors. + #[pyo3(name = "remove_op_node")] + fn py_remove_op_node(&mut self, node: &Bound) -> PyResult<()> { + let node: PyRef = match node.downcast::() { + Ok(node) => node.borrow(), + Err(_) => return Err(DAGCircuitError::new_err("Node not an DAGOpNode")), + }; + let index = node.as_ref().node.unwrap(); + if self.dag.node_weight(index).is_none() { + return Err(DAGCircuitError::new_err("Node not in DAG")); + } + self.remove_op_node(index); + Ok(()) + } + + /// Remove all of the ancestor operation nodes of node. + fn remove_ancestors_of(&mut self, node: &DAGNode) -> PyResult<()> { + let ancestors: Vec<_> = core_ancestors(&self.dag, node.node.unwrap()) + .filter(|next| { + next != &node.node.unwrap() + && matches!(self.dag.node_weight(*next), Some(NodeType::Operation(_))) + }) + .collect(); + for a in ancestors { + self.dag.remove_node(a); + } + Ok(()) + } + + /// Remove all of the descendant operation nodes of node. + fn remove_descendants_of(&mut self, node: &DAGNode) -> PyResult<()> { + let descendants: Vec<_> = core_descendants(&self.dag, node.node.unwrap()) + .filter(|next| { + next != &node.node.unwrap() + && matches!(self.dag.node_weight(*next), Some(NodeType::Operation(_))) + }) + .collect(); + for d in descendants { + self.dag.remove_node(d); + } + Ok(()) + } + + /// Remove all of the non-ancestors operation nodes of node. + fn remove_nonancestors_of(&mut self, node: &DAGNode) -> PyResult<()> { + let ancestors: HashSet<_> = core_ancestors(&self.dag, node.node.unwrap()) + .filter(|next| { + next != &node.node.unwrap() + && matches!(self.dag.node_weight(*next), Some(NodeType::Operation(_))) + }) + .collect(); + let non_ancestors: Vec<_> = self + .dag + .node_indices() + .filter(|node_id| !ancestors.contains(node_id)) + .collect(); + for na in non_ancestors { + self.dag.remove_node(na); + } + Ok(()) + } + + /// Remove all of the non-descendants operation nodes of node. + fn remove_nondescendants_of(&mut self, node: &DAGNode) -> PyResult<()> { + let descendants: HashSet<_> = core_descendants(&self.dag, node.node.unwrap()) + .filter(|next| { + next != &node.node.unwrap() + && matches!(self.dag.node_weight(*next), Some(NodeType::Operation(_))) + }) + .collect(); + let non_descendants: Vec<_> = self + .dag + .node_indices() + .filter(|node_id| !descendants.contains(node_id)) + .collect(); + for nd in non_descendants { + self.dag.remove_node(nd); + } + Ok(()) + } + + /// Return a list of op nodes in the first layer of this dag. + #[pyo3(name = "front_layer")] + fn py_front_layer(&self, py: Python) -> PyResult> { + let native_front_layer = self.front_layer(py); + let front_layer_list = PyList::empty_bound(py); + for node in native_front_layer { + front_layer_list.append(self.get_node(py, node)?)?; + } + Ok(front_layer_list.into()) + } + + /// Yield a shallow view on a layer of this DAGCircuit for all d layers of this circuit. + /// + /// A layer is a circuit whose gates act on disjoint qubits, i.e., + /// a layer has depth 1. The total number of layers equals the + /// circuit depth d. The layers are indexed from 0 to d-1 with the + /// earliest layer at index 0. The layers are constructed using a + /// greedy algorithm. Each returned layer is a dict containing + /// {"graph": circuit graph, "partition": list of qubit lists}. + /// + /// The returned layer contains new (but semantically equivalent) DAGOpNodes, DAGInNodes, + /// and DAGOutNodes. These are not the same as nodes of the original dag, but are equivalent + /// via DAGNode.semantic_eq(node1, node2). + /// + /// TODO: Gates that use the same cbits will end up in different + /// layers as this is currently implemented. This may not be + /// the desired behavior. + #[pyo3(signature = (*, vars_mode="captures"))] + fn layers(&self, py: Python, vars_mode: &str) -> PyResult> { + let layer_list = PyList::empty_bound(py); + let mut graph_layers = self.multigraph_layers(py); + if graph_layers.next().is_none() { + return Ok(PyIterator::from_bound_object(&layer_list)?.into()); + } + + for graph_layer in graph_layers { + let layer_dict = PyDict::new_bound(py); + // Sort to make sure they are in the order they were added to the original DAG + // It has to be done by node_id as graph_layer is just a list of nodes + // with no implied topology + // Drawing tools rely on _node_id to infer order of node creation + // so we need this to be preserved by layers() + // Get the op nodes from the layer, removing any input and output nodes. + let mut op_nodes: Vec<(&PackedInstruction, &NodeIndex)> = graph_layer + .iter() + .filter_map(|node| self.dag.node_weight(*node).map(|dag_node| (dag_node, node))) + .filter_map(|(node, index)| match node { + NodeType::Operation(oper) => Some((oper, index)), + _ => None, + }) + .collect(); + op_nodes.sort_by_key(|(_, node_index)| **node_index); + + if op_nodes.is_empty() { + return Ok(PyIterator::from_bound_object(&layer_list)?.into()); + } + + let mut new_layer = self.copy_empty_like(py, vars_mode)?; + + for (node, _) in op_nodes { + new_layer.push_back(py, node.clone())?; + } + + let new_layer_op_nodes = new_layer.op_nodes(false).filter_map(|node_index| { + match new_layer.dag.node_weight(node_index) { + Some(NodeType::Operation(ref node)) => Some(node), + _ => None, + } + }); + let support_iter = new_layer_op_nodes.into_iter().map(|node| { + PyTuple::new_bound( + py, + new_layer + .qubits + .map_indices(new_layer.qargs_cache.intern(node.qubits)), + ) + }); + let support_list = PyList::empty_bound(py); + for support_qarg in support_iter { + support_list.append(support_qarg)?; + } + layer_dict.set_item("graph", new_layer.into_py(py))?; + layer_dict.set_item("partition", support_list)?; + layer_list.append(layer_dict)?; + } + Ok(layer_list.into_any().iter()?.into()) + } + + /// Yield a layer for all gates of this circuit. + /// + /// A serial layer is a circuit with one gate. The layers have the + /// same structure as in layers(). + #[pyo3(signature = (*, vars_mode="captures"))] + fn serial_layers(&self, py: Python, vars_mode: &str) -> PyResult> { + let layer_list = PyList::empty_bound(py); + for next_node in self.topological_op_nodes()? { + let retrieved_node: &PackedInstruction = match self.dag.node_weight(next_node) { + Some(NodeType::Operation(node)) => node, + _ => unreachable!("A non-operation node was obtained from topological_op_nodes."), + }; + let mut new_layer = self.copy_empty_like(py, vars_mode)?; + + // Save the support of the operation we add to the layer + let support_list = PyList::empty_bound(py); + let qubits = PyTuple::new_bound( + py, + self.qargs_cache + .intern(retrieved_node.qubits) + .iter() + .map(|qubit| self.qubits.get(*qubit)), + ) + .unbind(); + new_layer.push_back(py, retrieved_node.clone())?; + + if !retrieved_node.op.directive() { + support_list.append(qubits)?; + } + + let layer_dict = [ + ("graph", new_layer.into_py(py)), + ("partition", support_list.into_any().unbind()), + ] + .into_py_dict_bound(py); + layer_list.append(layer_dict)?; + } + + Ok(layer_list.into_any().iter()?.into()) + } + + /// Yield layers of the multigraph. + #[pyo3(name = "multigraph_layers")] + fn py_multigraph_layers(&self, py: Python) -> PyResult> { + let graph_layers = self.multigraph_layers(py).map(|layer| -> Vec { + layer + .into_iter() + .filter_map(|index| self.get_node(py, index).ok()) + .collect() + }); + let list: Bound = + PyList::new_bound(py, graph_layers.collect::>>()); + Ok(PyIterator::from_bound_object(&list)?.unbind()) + } + + /// Return a set of non-conditional runs of "op" nodes with the given names. + /// + /// For example, "... h q[0]; cx q[0],q[1]; cx q[0],q[1]; h q[1]; .." + /// would produce the tuple of cx nodes as an element of the set returned + /// from a call to collect_runs(["cx"]). If instead the cx nodes were + /// "cx q[0],q[1]; cx q[1],q[0];", the method would still return the + /// pair in a tuple. The namelist can contain names that are not + /// in the circuit's basis. + /// + /// Nodes must have only one successor to continue the run. + #[pyo3(name = "collect_runs")] + fn py_collect_runs(&self, py: Python, namelist: &Bound) -> PyResult> { + let mut name_list_set = HashSet::with_capacity(namelist.len()); + for name in namelist.iter() { + name_list_set.insert(name.extract::()?); + } + match self.collect_runs(name_list_set) { + Some(runs) => { + let run_iter = runs.map(|node_indices| { + PyTuple::new_bound( + py, + node_indices + .into_iter() + .map(|node_index| self.get_node(py, node_index).unwrap()), + ) + .unbind() + }); + let out_set = PySet::empty_bound(py)?; + for run_tuple in run_iter { + out_set.add(run_tuple)?; + } + Ok(out_set.unbind()) + } + None => Err(PyRuntimeError::new_err( + "Invalid DAGCircuit, cycle encountered", + )), + } + } + + /// Return a set of non-conditional runs of 1q "op" nodes. + #[pyo3(name = "collect_1q_runs")] + fn py_collect_1q_runs(&self, py: Python) -> PyResult> { + match self.collect_1q_runs() { + Some(runs) => { + let runs_iter = runs.map(|node_indices| { + PyList::new_bound( + py, + node_indices + .into_iter() + .map(|node_index| self.get_node(py, node_index).unwrap()), + ) + .unbind() + }); + let out_list = PyList::empty_bound(py); + for run_list in runs_iter { + out_list.append(run_list)?; + } + Ok(out_list.unbind()) + } + None => Err(PyRuntimeError::new_err( + "Invalid DAGCircuit, cycle encountered", + )), + } + } + + /// Return a set of non-conditional runs of 2q "op" nodes. + #[pyo3(name = "collect_2q_runs")] + fn py_collect_2q_runs(&self, py: Python) -> PyResult> { + match self.collect_2q_runs() { + Some(runs) => { + let runs_iter = runs.into_iter().map(|node_indices| { + PyList::new_bound( + py, + node_indices + .into_iter() + .map(|node_index| self.get_node(py, node_index).unwrap()), + ) + .unbind() + }); + let out_list = PyList::empty_bound(py); + for run_list in runs_iter { + out_list.append(run_list)?; + } + Ok(out_list.unbind()) + } + None => Err(PyRuntimeError::new_err( + "Invalid DAGCircuit, cycle encountered", + )), + } + } + + /// Iterator for nodes that affect a given wire. + /// + /// Args: + /// wire (Bit): the wire to be looked at. + /// only_ops (bool): True if only the ops nodes are wanted; + /// otherwise, all nodes are returned. + /// Yield: + /// Iterator: the successive nodes on the given wire + /// + /// Raises: + /// DAGCircuitError: if the given wire doesn't exist in the DAG + #[pyo3(name = "nodes_on_wire", signature = (wire, only_ops=false))] + fn py_nodes_on_wire( + &self, + py: Python, + wire: &Bound, + only_ops: bool, + ) -> PyResult> { + let wire = if wire.is_instance(imports::QUBIT.get_bound(py))? { + self.qubits.find(wire).map(Wire::Qubit) + } else if wire.is_instance(imports::CLBIT.get_bound(py))? { + self.clbits.find(wire).map(Wire::Clbit) + } else if self.var_input_map.contains_key(py, &wire.clone().unbind()) { + Some(Wire::Var(wire.clone().unbind())) + } else { + None + } + .ok_or_else(|| { + DAGCircuitError::new_err(format!( + "The given wire {:?} is not present in the circuit", + wire + )) + })?; + + let nodes = self + .nodes_on_wire(py, &wire, only_ops) + .into_iter() + .map(|n| self.get_node(py, n)) + .collect::>>()?; + Ok(PyTuple::new_bound(py, nodes).into_any().iter()?.unbind()) + } + + /// Count the occurrences of operation names. + /// + /// Args: + /// recurse: if ``True`` (default), then recurse into control-flow operations. In all + /// cases, this counts only the number of times the operation appears in any possible + /// block; both branches of if-elses are counted, and for- and while-loop blocks are + /// only counted once. + /// + /// Returns: + /// Mapping[str, int]: a mapping of operation names to the number of times it appears. + #[pyo3(signature = (*, recurse=true))] + fn count_ops(&self, py: Python, recurse: bool) -> PyResult { + if !recurse + || !CONTROL_FLOW_OP_NAMES + .iter() + .any(|x| self.op_names.contains_key(*x)) + { + Ok(self.op_names.to_object(py)) + } else { + fn inner( + py: Python, + dag: &DAGCircuit, + counts: &mut HashMap, + ) -> PyResult<()> { + for (key, value) in dag.op_names.iter() { + counts + .entry(key.clone()) + .and_modify(|count| *count += value) + .or_insert(*value); + } + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); + for node in dag.py_op_nodes( + py, + Some(imports::CONTROL_FLOW_OP.get_bound(py).downcast()?), + true, + )? { + let raw_blocks = node.getattr(py, "op")?.getattr(py, "blocks")?; + let blocks: &Bound = raw_blocks.downcast_bound::(py)?; + for block in blocks.iter() { + let inner_dag: &DAGCircuit = + &circuit_to_dag.call1((block.clone(),))?.extract()?; + inner(py, inner_dag, counts)?; + } + } + Ok(()) + } + let mut counts = HashMap::with_capacity(self.op_names.len()); + inner(py, self, &mut counts)?; + Ok(counts.to_object(py)) + } + } + + /// Count the occurrences of operation names on the longest path. + /// + /// Returns a dictionary of counts keyed on the operation name. + fn count_ops_longest_path(&self) -> PyResult> { + if self.dag.node_count() == 0 { + return Ok(HashMap::new()); + } + let weight_fn = |_| -> Result { Ok(1) }; + let longest_path = + match rustworkx_core::dag_algo::longest_path(&self.dag, weight_fn).unwrap() { + Some(res) => res.0, + None => return Err(DAGCircuitError::new_err("not a DAG")), + }; + // Allocate for worst case where all operations are unique + let mut op_counts: HashMap<&str, usize> = HashMap::with_capacity(longest_path.len() - 2); + for node_index in &longest_path[1..longest_path.len() - 1] { + if let NodeType::Operation(ref packed) = self.dag[*node_index] { + let name = packed.op.name(); + op_counts + .entry(name) + .and_modify(|count| *count += 1) + .or_insert(1); + } + } + Ok(op_counts) + } + + /// Returns causal cone of a qubit. + /// + /// A qubit's causal cone is the set of qubits that can influence the output of that + /// qubit through interactions, whether through multi-qubit gates or operations. Knowing + /// the causal cone of a qubit can be useful when debugging faulty circuits, as it can + /// help identify which wire(s) may be causing the problem. + /// + /// This method does not consider any classical data dependency in the ``DAGCircuit``, + /// classical bit wires are ignored for the purposes of building the causal cone. + /// + /// Args: + /// qubit (~qiskit.circuit.Qubit): The output qubit for which we want to find the causal cone. + /// + /// Returns: + /// Set[~qiskit.circuit.Qubit]: The set of qubits whose interactions affect ``qubit``. + fn quantum_causal_cone(&self, py: Python, qubit: &Bound) -> PyResult> { + // Retrieve the output node from the qubit + let output_qubit = self.qubits.find(qubit).ok_or_else(|| { + DAGCircuitError::new_err(format!( + "The given qubit {:?} is not present in the circuit", + qubit + )) + })?; + let output_node_index = self + .qubit_io_map + .get(output_qubit.0 as usize) + .map(|x| x[1]) + .ok_or_else(|| { + DAGCircuitError::new_err(format!( + "The given qubit {:?} is not present in qubit_output_map", + qubit + )) + })?; + + let mut qubits_in_cone: HashSet<&Qubit> = HashSet::from([&output_qubit]); + let mut queue: VecDeque = self.quantum_predecessors(output_node_index).collect(); + + // The processed_non_directive_nodes stores the set of processed non-directive nodes. + // This is an optimization to avoid considering the same non-directive node multiple + // times when reached from different paths. + // The directive nodes (such as barriers or measures) are trickier since when processing + // them we only add their predecessors that intersect qubits_in_cone. Hence, directive + // nodes have to be considered multiple times. + let mut processed_non_directive_nodes: HashSet = HashSet::new(); + + while !queue.is_empty() { + let cur_index = queue.pop_front().unwrap(); + + if let NodeType::Operation(packed) = self.dag.node_weight(cur_index).unwrap() { + if !packed.op.directive() { + // If the operation is not a directive (in particular not a barrier nor a measure), + // we do not do anything if it was already processed. Otherwise, we add its qubits + // to qubits_in_cone, and append its predecessors to queue. + if processed_non_directive_nodes.contains(&cur_index) { + continue; + } + qubits_in_cone.extend(self.qargs_cache.intern(packed.qubits).iter()); + processed_non_directive_nodes.insert(cur_index); + + for pred_index in self.quantum_predecessors(cur_index) { + if let NodeType::Operation(_pred_packed) = + self.dag.node_weight(pred_index).unwrap() + { + queue.push_back(pred_index); + } + } + } else { + // Directives (such as barriers and measures) may be defined over all the qubits, + // yet not all of these qubits should be considered in the causal cone. So we + // only add those predecessors that have qubits in common with qubits_in_cone. + for pred_index in self.quantum_predecessors(cur_index) { + if let NodeType::Operation(pred_packed) = + self.dag.node_weight(pred_index).unwrap() + { + if self + .qargs_cache + .intern(pred_packed.qubits) + .iter() + .any(|x| qubits_in_cone.contains(x)) + { + queue.push_back(pred_index); + } + } + } + } + } + } + + let qubits_in_cone_vec: Vec<_> = qubits_in_cone.iter().map(|&&qubit| qubit).collect(); + let elements = self.qubits.map_indices(&qubits_in_cone_vec[..]); + Ok(PySet::new_bound(py, elements)?.unbind()) + } + + /// Return a dictionary of circuit properties. + fn properties(&self, py: Python) -> PyResult> { + Ok(HashMap::from_iter([ + ("size", self.size(py, false)?.into_py(py)), + ("depth", self.depth(py, false)?.into_py(py)), + ("width", self.width().into_py(py)), + ("qubits", self.num_qubits().into_py(py)), + ("bits", self.num_clbits().into_py(py)), + ("factors", self.num_tensor_factors().into_py(py)), + ("operations", self.count_ops(py, true)?), + ])) + } + + /// Draws the dag circuit. + /// + /// This function needs `Graphviz `_ to be + /// installed. Graphviz is not a python package and can't be pip installed + /// (the ``graphviz`` package on PyPI is a Python interface library for + /// Graphviz and does not actually install Graphviz). You can refer to + /// `the Graphviz documentation `__ on + /// how to install it. + /// + /// Args: + /// scale (float): scaling factor + /// filename (str): file path to save image to (format inferred from name) + /// style (str): + /// 'plain': B&W graph; + /// 'color' (default): color input/output/op nodes + /// + /// Returns: + /// Ipython.display.Image: if in Jupyter notebook and not saving to file, + /// otherwise None. + #[pyo3(signature=(scale=0.7, filename=None, style="color"))] + fn draw<'py>( + slf: PyRef<'py, Self>, + py: Python<'py>, + scale: f64, + filename: Option<&str>, + style: &str, + ) -> PyResult> { + let module = PyModule::import_bound(py, "qiskit.visualization.dag_visualization")?; + module.call_method1("dag_drawer", (slf, scale, filename, style)) + } + + fn _to_dot<'py>( + &self, + py: Python<'py>, + graph_attrs: Option>, + node_attrs: Option, + edge_attrs: Option, + ) -> PyResult> { + let mut buffer = Vec::::new(); + build_dot(py, self, &mut buffer, graph_attrs, node_attrs, edge_attrs)?; + Ok(PyString::new_bound(py, std::str::from_utf8(&buffer)?)) + } + + /// Add an input variable to the circuit. + /// + /// Args: + /// var: the variable to add. + fn add_input_var(&mut self, py: Python, var: &Bound) -> PyResult<()> { + if !self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .is_empty() + { + return Err(DAGCircuitError::new_err( + "cannot add inputs to a circuit with captures", + )); + } + self.add_var(py, var, DAGVarType::Input) + } + + /// Add a captured variable to the circuit. + /// + /// Args: + /// var: the variable to add. + fn add_captured_var(&mut self, py: Python, var: &Bound) -> PyResult<()> { + if !self.vars_by_type[DAGVarType::Input as usize] + .bind(py) + .is_empty() + { + return Err(DAGCircuitError::new_err( + "cannot add captures to a circuit with inputs", + )); + } + self.add_var(py, var, DAGVarType::Capture) + } + + /// Add a declared local variable to the circuit. + /// + /// Args: + /// var: the variable to add. + fn add_declared_var(&mut self, py: Python, var: &Bound) -> PyResult<()> { + self.add_var(py, var, DAGVarType::Declare) + } + + /// Total number of classical variables tracked by the circuit. + #[getter] + fn num_vars(&self) -> usize { + self.vars_info.len() + } + + /// Number of input classical variables tracked by the circuit. + #[getter] + fn num_input_vars(&self, py: Python) -> usize { + self.vars_by_type[DAGVarType::Input as usize].bind(py).len() + } + + /// Number of captured classical variables tracked by the circuit. + #[getter] + fn num_captured_vars(&self, py: Python) -> usize { + self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .len() + } + + /// Number of declared local classical variables tracked by the circuit. + #[getter] + fn num_declared_vars(&self, py: Python) -> usize { + self.vars_by_type[DAGVarType::Declare as usize] + .bind(py) + .len() + } + + /// Is this realtime variable in the DAG? + /// + /// Args: + /// var: the variable or name to check. + fn has_var(&self, var: &Bound) -> PyResult { + match var.extract::() { + Ok(name) => Ok(self.vars_info.contains_key(&name)), + Err(_) => { + let raw_name = var.getattr("name")?; + let var_name: String = raw_name.extract()?; + match self.vars_info.get(&var_name) { + Some(var_in_dag) => Ok(var_in_dag.var.is(var)), + None => Ok(false), + } + } + } + } + + /// Iterable over the input classical variables tracked by the circuit. + fn iter_input_vars(&self, py: Python) -> PyResult> { + Ok(self.vars_by_type[DAGVarType::Input as usize] + .bind(py) + .clone() + .into_any() + .iter()? + .unbind()) + } + + /// Iterable over the captured classical variables tracked by the circuit. + fn iter_captured_vars(&self, py: Python) -> PyResult> { + Ok(self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .clone() + .into_any() + .iter()? + .unbind()) + } + + /// Iterable over the declared classical variables tracked by the circuit. + fn iter_declared_vars(&self, py: Python) -> PyResult> { + Ok(self.vars_by_type[DAGVarType::Declare as usize] + .bind(py) + .clone() + .into_any() + .iter()? + .unbind()) + } + + /// Iterable over all the classical variables tracked by the circuit. + fn iter_vars(&self, py: Python) -> PyResult> { + let out_set = PySet::empty_bound(py)?; + for var_type_set in &self.vars_by_type { + for var in var_type_set.bind(py).iter() { + out_set.add(var)?; + } + } + Ok(out_set.into_any().iter()?.unbind()) + } + + fn _has_edge(&self, source: usize, target: usize) -> bool { + self.dag + .contains_edge(NodeIndex::new(source), NodeIndex::new(target)) + } + + fn _is_dag(&self) -> bool { + rustworkx_core::petgraph::algo::toposort(&self.dag, None).is_ok() + } + + fn _in_edges(&self, py: Python, node_index: usize) -> Vec> { + self.dag + .edges_directed(NodeIndex::new(node_index), Incoming) + .map(|wire| { + ( + wire.source().index(), + wire.target().index(), + match wire.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }, + ) + .into_py(py) + }) + .collect() + } + + fn _out_edges(&self, py: Python, node_index: usize) -> Vec> { + self.dag + .edges_directed(NodeIndex::new(node_index), Outgoing) + .map(|wire| { + ( + wire.source().index(), + wire.target().index(), + match wire.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }, + ) + .into_py(py) + }) + .collect() + } + + fn _in_wires(&self, node_index: usize) -> Vec<&PyObject> { + self.dag + .edges_directed(NodeIndex::new(node_index), Incoming) + .map(|wire| match wire.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }) + .collect() + } + + fn _out_wires(&self, node_index: usize) -> Vec<&PyObject> { + self.dag + .edges_directed(NodeIndex::new(node_index), Outgoing) + .map(|wire| match wire.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }) + .collect() + } + + fn _find_successors_by_edge( + &self, + py: Python, + node_index: usize, + edge_checker: &Bound, + ) -> PyResult> { + let mut result = Vec::new(); + for e in self + .dag + .edges_directed(NodeIndex::new(node_index), Outgoing) + .unique_by(|e| e.id()) + { + let weight = match e.weight() { + Wire::Qubit(q) => self.qubits.get(*q).unwrap(), + Wire::Clbit(c) => self.clbits.get(*c).unwrap(), + Wire::Var(v) => v, + }; + if edge_checker.call1((weight,))?.extract::()? { + result.push(self.get_node(py, e.target())?); + } + } + Ok(result) + } + + fn _insert_1q_on_incoming_qubit( + &mut self, + py: Python, + node: &Bound, + old_index: usize, + ) -> PyResult<()> { + if let NodeType::Operation(inst) = self.pack_into(py, node)? { + self.increment_op(inst.op.name()); + let new_index = self.dag.add_node(NodeType::Operation(inst)); + let old_index: NodeIndex = NodeIndex::new(old_index); + let (parent_index, edge_index, weight) = self + .dag + .edges_directed(old_index, Incoming) + .map(|edge| (edge.source(), edge.id(), edge.weight().clone())) + .next() + .unwrap(); + self.dag.add_edge(parent_index, new_index, weight.clone()); + self.dag.add_edge(new_index, old_index, weight); + self.dag.remove_edge(edge_index); + Ok(()) + } else { + Err(PyTypeError::new_err("Invalid node type input")) + } + } + + fn _edges(&self, py: Python) -> Vec { + self.dag + .edge_indices() + .map(|index| { + let wire = self.dag.edge_weight(index).unwrap(); + match wire { + Wire::Qubit(qubit) => self.qubits.get(*qubit).to_object(py), + Wire::Clbit(clbit) => self.clbits.get(*clbit).to_object(py), + Wire::Var(var) => var.clone_ref(py), + } + }) + .collect() + } +} + +impl DAGCircuit { + /// Return an iterator of gate runs with non-conditional op nodes of given names + pub fn collect_runs( + &self, + namelist: HashSet, + ) -> Option> + '_> { + let filter_fn = move |node_index: NodeIndex| -> Result { + let node = &self.dag[node_index]; + match node { + NodeType::Operation(inst) => Ok(namelist.contains(inst.op.name()) + && match &inst.extra_attrs { + None => true, + Some(attrs) => attrs.condition.is_none(), + }), + _ => Ok(false), + } + }; + rustworkx_core::dag_algo::collect_runs(&self.dag, filter_fn) + .map(|node_iter| node_iter.map(|x| x.unwrap())) + } + + /// Return a set of non-conditional runs of 1q "op" nodes. + pub fn collect_1q_runs(&self) -> Option> + '_> { + let filter_fn = move |node_index: NodeIndex| -> Result { + let node = &self.dag[node_index]; + match node { + NodeType::Operation(inst) => Ok(inst.op.num_qubits() == 1 + && inst.op.num_clbits() == 0 + && !inst.is_parameterized() + && (inst.op.try_standard_gate().is_some() + || inst.op.matrix(inst.params_view()).is_some()) + && inst.condition().is_none()), + _ => Ok(false), + } + }; + rustworkx_core::dag_algo::collect_runs(&self.dag, filter_fn) + .map(|node_iter| node_iter.map(|x| x.unwrap())) + } + + /// Return a set of non-conditional runs of 2q "op" nodes. + pub fn collect_2q_runs(&self) -> Option>> { + let filter_fn = move |node_index: NodeIndex| -> Result, Infallible> { + let node = &self.dag[node_index]; + match node { + NodeType::Operation(inst) => match inst.op.view() { + OperationRef::Standard(gate) => Ok(Some( + gate.num_qubits() <= 2 + && inst.condition().is_none() + && !inst.is_parameterized(), + )), + OperationRef::Gate(gate) => Ok(Some( + gate.num_qubits() <= 2 + && inst.condition().is_none() + && !inst.is_parameterized(), + )), + _ => Ok(Some(false)), + }, + _ => Ok(None), + } + }; + + let color_fn = move |edge_index: EdgeIndex| -> Result, Infallible> { + let wire = self.dag.edge_weight(edge_index).unwrap(); + match wire { + Wire::Qubit(index) => Ok(Some(index.0 as usize)), + _ => Ok(None), + } + }; + rustworkx_core::dag_algo::collect_bicolor_runs(&self.dag, filter_fn, color_fn).unwrap() + } + + fn increment_op(&mut self, op: &str) { + match self.op_names.get_mut(op) { + Some(count) => { + *count += 1; + } + None => { + self.op_names.insert(op.to_string(), 1); + } + } + } + + fn decrement_op(&mut self, op: &str) { + match self.op_names.get_mut(op) { + Some(count) => { + if *count > 1 { + *count -= 1; + } else { + self.op_names.swap_remove(op); + } + } + None => panic!("Cannot decrement something not added!"), + } + } + + fn quantum_predecessors(&self, node: NodeIndex) -> impl Iterator + '_ { + self.dag + .edges_directed(node, Incoming) + .filter_map(|e| match e.weight() { + Wire::Qubit(_) => Some(e.source()), + _ => None, + }) + .unique() + } + + fn quantum_successors(&self, node: NodeIndex) -> impl Iterator + '_ { + self.dag + .edges_directed(node, Outgoing) + .filter_map(|e| match e.weight() { + Wire::Qubit(_) => Some(e.target()), + _ => None, + }) + .unique() + } + + /// Apply a [PackedInstruction] to the back of the circuit. + /// + /// The provided `instr` MUST be valid for this DAG, e.g. its + /// bits, registers, vars, and interner IDs must be valid in + /// this DAG. + /// + /// This is mostly used to apply operations from one DAG to + /// another that was created from the first via + /// [DAGCircuit::copy_empty_like]. + fn push_back(&mut self, py: Python, instr: PackedInstruction) -> PyResult { + let op_name = instr.op.name(); + let (all_cbits, vars): (Vec, Option>) = { + if self.may_have_additional_wires(py, &instr) { + let mut clbits: HashSet = + HashSet::from_iter(self.cargs_cache.intern(instr.clbits).iter().copied()); + let (additional_clbits, additional_vars) = + self.additional_wires(py, instr.op.view(), instr.condition())?; + for clbit in additional_clbits { + clbits.insert(clbit); + } + (clbits.into_iter().collect(), Some(additional_vars)) + } else { + (self.cargs_cache.intern(instr.clbits).to_vec(), None) + } + }; + + self.increment_op(op_name); + + let qubits_id = instr.qubits; + let new_node = self.dag.add_node(NodeType::Operation(instr)); + + // Put the new node in-between the previously "last" nodes on each wire + // and the output map. + let output_nodes: HashSet = self + .qargs_cache + .intern(qubits_id) + .iter() + .map(|q| self.qubit_io_map.get(q.0 as usize).map(|x| x[1]).unwrap()) + .chain( + all_cbits + .iter() + .map(|c| self.clbit_io_map.get(c.0 as usize).map(|x| x[1]).unwrap()), + ) + .chain( + vars.iter() + .flatten() + .map(|v| self.var_output_map.get(py, v).unwrap()), + ) + .collect(); + + for output_node in output_nodes { + let last_edges: Vec<_> = self + .dag + .edges_directed(output_node, Incoming) + .map(|e| (e.source(), e.id(), e.weight().clone())) + .collect(); + for (source, old_edge, weight) in last_edges.into_iter() { + self.dag.add_edge(source, new_node, weight.clone()); + self.dag.add_edge(new_node, output_node, weight); + self.dag.remove_edge(old_edge); + } + } + + Ok(new_node) + } + + /// Apply a [PackedInstruction] to the front of the circuit. + /// + /// The provided `instr` MUST be valid for this DAG, e.g. its + /// bits, registers, vars, and interner IDs must be valid in + /// this DAG. + /// + /// This is mostly used to apply operations from one DAG to + /// another that was created from the first via + /// [DAGCircuit::copy_empty_like]. + fn push_front(&mut self, py: Python, inst: PackedInstruction) -> PyResult { + let op_name = inst.op.name(); + let (all_cbits, vars): (Vec, Option>) = { + if self.may_have_additional_wires(py, &inst) { + let mut clbits: HashSet = + HashSet::from_iter(self.cargs_cache.intern(inst.clbits).iter().cloned()); + let (additional_clbits, additional_vars) = + self.additional_wires(py, inst.op.view(), inst.condition())?; + for clbit in additional_clbits { + clbits.insert(clbit); + } + (clbits.into_iter().collect(), Some(additional_vars)) + } else { + (self.cargs_cache.intern(inst.clbits).to_vec(), None) + } + }; + + self.increment_op(op_name); + + let qubits_id = inst.qubits; + let new_node = self.dag.add_node(NodeType::Operation(inst)); + + // Put the new node in-between the input map and the previously + // "first" nodes on each wire. + let mut input_nodes: Vec = self + .qargs_cache + .intern(qubits_id) + .iter() + .map(|q| self.qubit_io_map[q.0 as usize][0]) + .chain(all_cbits.iter().map(|c| self.clbit_io_map[c.0 as usize][0])) + .collect(); + if let Some(vars) = vars { + for var in vars { + input_nodes.push(self.var_input_map.get(py, &var).unwrap()); + } + } + + for input_node in input_nodes { + let first_edges: Vec<_> = self + .dag + .edges_directed(input_node, Outgoing) + .map(|e| (e.target(), e.id(), e.weight().clone())) + .collect(); + for (target, old_edge, weight) in first_edges.into_iter() { + self.dag.add_edge(input_node, new_node, weight.clone()); + self.dag.add_edge(new_node, target, weight); + self.dag.remove_edge(old_edge); + } + } + + Ok(new_node) + } + + fn sort_key(&self, node: NodeIndex) -> SortKeyType { + match &self.dag[node] { + NodeType::Operation(packed) => ( + self.qargs_cache.intern(packed.qubits).as_slice(), + self.cargs_cache.intern(packed.clbits).as_slice(), + ), + NodeType::QubitIn(q) => (std::slice::from_ref(q), &[Clbit(u32::MAX)]), + NodeType::QubitOut(_q) => (&[Qubit(u32::MAX)], &[Clbit(u32::MAX)]), + NodeType::ClbitIn(c) => (&[Qubit(u32::MAX)], std::slice::from_ref(c)), + NodeType::ClbitOut(_c) => (&[Qubit(u32::MAX)], &[Clbit(u32::MAX)]), + _ => (&[], &[]), + } + } + + fn topological_nodes(&self) -> PyResult> { + let key = |node: NodeIndex| -> Result { Ok(self.sort_key(node)) }; + let nodes = + rustworkx_core::dag_algo::lexicographical_topological_sort(&self.dag, key, false, None) + .map_err(|e| match e { + rustworkx_core::dag_algo::TopologicalSortError::CycleOrBadInitialState => { + PyValueError::new_err(format!("{}", e)) + } + rustworkx_core::dag_algo::TopologicalSortError::KeyError(_) => { + unreachable!() + } + })?; + Ok(nodes.into_iter()) + } + + fn topological_op_nodes(&self) -> PyResult + '_> { + Ok(self.topological_nodes()?.filter(|node: &NodeIndex| { + matches!(self.dag.node_weight(*node), Some(NodeType::Operation(_))) + })) + } + + fn topological_key_sort( + &self, + py: Python, + key: &Bound, + ) -> PyResult> { + // This path (user provided key func) is not ideal, since we no longer + // use a string key after moving to Rust, in favor of using a tuple + // of the qargs and cargs interner IDs of the node. + let key = |node: NodeIndex| -> PyResult { + let node = self.get_node(py, node)?; + key.call1((node,))?.extract() + }; + Ok( + rustworkx_core::dag_algo::lexicographical_topological_sort(&self.dag, key, false, None) + .map_err(|e| match e { + rustworkx_core::dag_algo::TopologicalSortError::CycleOrBadInitialState => { + PyValueError::new_err(format!("{}", e)) + } + rustworkx_core::dag_algo::TopologicalSortError::KeyError(ref e) => { + e.clone_ref(py) + } + })? + .into_iter(), + ) + } + + fn is_wire_idle(&self, py: Python, wire: &Wire) -> PyResult { + let (input_node, output_node) = match wire { + Wire::Qubit(qubit) => ( + self.qubit_io_map[qubit.0 as usize][0], + self.qubit_io_map[qubit.0 as usize][1], + ), + Wire::Clbit(clbit) => ( + self.clbit_io_map[clbit.0 as usize][0], + self.clbit_io_map[clbit.0 as usize][1], + ), + Wire::Var(var) => ( + self.var_input_map.get(py, var).unwrap(), + self.var_output_map.get(py, var).unwrap(), + ), + }; + + let child = self + .dag + .neighbors_directed(input_node, Outgoing) + .next() + .ok_or_else(|| { + DAGCircuitError::new_err(format!( + "Invalid dagcircuit input node {:?} has no output", + input_node + )) + })?; + + Ok(child == output_node) + } + + fn may_have_additional_wires(&self, py: Python, instr: &PackedInstruction) -> bool { + if instr.condition().is_some() { + return true; + } + let OperationRef::Instruction(inst) = instr.op.view() else { + return false; + }; + inst.control_flow() + || inst + .instruction + .bind(py) + .is_instance(imports::STORE_OP.get_bound(py)) + .unwrap() + } + + fn additional_wires( + &self, + py: Python, + op: OperationRef, + condition: Option<&PyObject>, + ) -> PyResult<(Vec, Vec)> { + let wires_from_expr = |node: &Bound| -> PyResult<(Vec, Vec)> { + let mut clbits = Vec::new(); + let mut vars = Vec::new(); + for var in imports::ITER_VARS.get_bound(py).call1((node,))?.iter()? { + let var = var?; + let var_var = var.getattr("var")?; + if var_var.is_instance(imports::CLBIT.get_bound(py))? { + clbits.push(self.clbits.find(&var_var).unwrap()); + } else if var_var.is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? { + for bit in var_var.iter().unwrap() { + clbits.push(self.clbits.find(&bit?).unwrap()); + } + } else { + vars.push(var.unbind()); + } + } + Ok((clbits, vars)) + }; + + let mut clbits = Vec::new(); + let mut vars = Vec::new(); + if let Some(condition) = condition { + let condition = condition.bind(py); + if !condition.is_none() { + if condition.is_instance(imports::EXPR.get_bound(py)).unwrap() { + let (expr_clbits, expr_vars) = wires_from_expr(condition)?; + for bit in expr_clbits { + clbits.push(bit); + } + for var in expr_vars { + vars.push(var); + } + } else { + for bit in self + .control_flow_module + .condition_resources(condition)? + .clbits + .bind(py) + { + clbits.push(self.clbits.find(&bit).unwrap()); + } + } + } + } + + if let OperationRef::Instruction(inst) = op { + let op = inst.instruction.bind(py); + if inst.control_flow() { + for var in op.call_method0("iter_captured_vars")?.iter()? { + vars.push(var?.unbind()) + } + if op.is_instance(imports::SWITCH_CASE_OP.get_bound(py))? { + let target = op.getattr(intern!(py, "target"))?; + if target.is_instance(imports::CLBIT.get_bound(py))? { + clbits.push(self.clbits.find(&target).unwrap()); + } else if target.is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? { + for bit in target.iter()? { + clbits.push(self.clbits.find(&bit?).unwrap()); + } + } else { + let (expr_clbits, expr_vars) = wires_from_expr(&target)?; + for bit in expr_clbits { + clbits.push(bit); + } + for var in expr_vars { + vars.push(var); + } + } + } + } else if op.is_instance(imports::STORE_OP.get_bound(py))? { + let (expr_clbits, expr_vars) = wires_from_expr(&op.getattr("lvalue")?)?; + for bit in expr_clbits { + clbits.push(bit); + } + for var in expr_vars { + vars.push(var); + } + let (expr_clbits, expr_vars) = wires_from_expr(&op.getattr("rvalue")?)?; + for bit in expr_clbits { + clbits.push(bit); + } + for var in expr_vars { + vars.push(var); + } + } + } + Ok((clbits, vars)) + } + + /// Add a qubit or bit to the circuit. + /// + /// Args: + /// wire: the wire to be added + /// + /// This adds a pair of in and out nodes connected by an edge. + /// + /// Raises: + /// DAGCircuitError: if trying to add duplicate wire + fn add_wire(&mut self, py: Python, wire: Wire) -> PyResult<()> { + let (in_node, out_node) = match wire { + Wire::Qubit(qubit) => { + if (qubit.0 as usize) >= self.qubit_io_map.len() { + let input_node = self.dag.add_node(NodeType::QubitIn(qubit)); + let output_node = self.dag.add_node(NodeType::QubitOut(qubit)); + self.qubit_io_map.push([input_node, output_node]); + Ok((input_node, output_node)) + } else { + Err(DAGCircuitError::new_err("qubit wire already exists!")) + } + } + Wire::Clbit(clbit) => { + if (clbit.0 as usize) >= self.clbit_io_map.len() { + let input_node = self.dag.add_node(NodeType::ClbitIn(clbit)); + let output_node = self.dag.add_node(NodeType::ClbitOut(clbit)); + self.clbit_io_map.push([input_node, output_node]); + Ok((input_node, output_node)) + } else { + Err(DAGCircuitError::new_err("classical wire already exists!")) + } + } + Wire::Var(ref var) => { + if self.var_input_map.contains_key(py, var) + || self.var_output_map.contains_key(py, var) + { + return Err(DAGCircuitError::new_err("var wire already exists!")); + } + let in_node = self.dag.add_node(NodeType::VarIn(var.clone_ref(py))); + let out_node = self.dag.add_node(NodeType::VarOut(var.clone_ref(py))); + self.var_input_map.insert(py, var.clone_ref(py), in_node); + self.var_output_map.insert(py, var.clone_ref(py), out_node); + Ok((in_node, out_node)) + } + }?; + + self.dag.add_edge(in_node, out_node, wire); + Ok(()) + } + + /// Get the nodes on the given wire. + /// + /// Note: result is empty if the wire is not in the DAG. + fn nodes_on_wire(&self, py: Python, wire: &Wire, only_ops: bool) -> Vec { + let mut nodes = Vec::new(); + let mut current_node = match wire { + Wire::Qubit(qubit) => self.qubit_io_map.get(qubit.0 as usize).map(|x| x[0]), + Wire::Clbit(clbit) => self.clbit_io_map.get(clbit.0 as usize).map(|x| x[0]), + Wire::Var(var) => self.var_input_map.get(py, var), + }; + + while let Some(node) = current_node { + if only_ops { + let node_weight = self.dag.node_weight(node).unwrap(); + if let NodeType::Operation(_) = node_weight { + nodes.push(node); + } + } else { + nodes.push(node); + } + + let edges = self.dag.edges_directed(node, Outgoing); + current_node = edges.into_iter().find_map(|edge| { + if edge.weight() == wire { + Some(edge.target()) + } else { + None + } + }); + } + nodes + } + + fn remove_idle_wire(&mut self, py: Python, wire: Wire) -> PyResult<()> { + let [in_node, out_node] = match wire { + Wire::Qubit(qubit) => self.qubit_io_map[qubit.0 as usize], + Wire::Clbit(clbit) => self.clbit_io_map[clbit.0 as usize], + Wire::Var(var) => [ + self.var_input_map.remove(py, &var).unwrap(), + self.var_output_map.remove(py, &var).unwrap(), + ], + }; + self.dag.remove_node(in_node); + self.dag.remove_node(out_node); + Ok(()) + } + + fn add_qubit_unchecked(&mut self, py: Python, bit: &Bound) -> PyResult { + let qubit = self.qubits.add(py, bit, false)?; + self.qubit_locations.bind(py).set_item( + bit, + Py::new( + py, + BitLocations { + index: (self.qubits.len() - 1), + registers: PyList::empty_bound(py).unbind(), + }, + )?, + )?; + self.add_wire(py, Wire::Qubit(qubit))?; + Ok(qubit) + } + + fn add_clbit_unchecked(&mut self, py: Python, bit: &Bound) -> PyResult { + let clbit = self.clbits.add(py, bit, false)?; + self.clbit_locations.bind(py).set_item( + bit, + Py::new( + py, + BitLocations { + index: (self.clbits.len() - 1), + registers: PyList::empty_bound(py).unbind(), + }, + )?, + )?; + self.add_wire(py, Wire::Clbit(clbit))?; + Ok(clbit) + } + + pub fn get_node(&self, py: Python, node: NodeIndex) -> PyResult> { + self.unpack_into(py, node, self.dag.node_weight(node).unwrap()) + } + + /// Remove an operation node n. + /// + /// Add edges from predecessors to successors. + fn remove_op_node(&mut self, index: NodeIndex) { + let mut edge_list: Vec<(NodeIndex, NodeIndex, Wire)> = Vec::new(); + for (source, in_weight) in self + .dag + .edges_directed(index, Incoming) + .map(|x| (x.source(), x.weight())) + { + for (target, out_weight) in self + .dag + .edges_directed(index, Outgoing) + .map(|x| (x.target(), x.weight())) + { + if in_weight == out_weight { + edge_list.push((source, target, in_weight.clone())); + } + } + } + for (source, target, weight) in edge_list { + self.dag.add_edge(source, target, weight); + } + + match self.dag.remove_node(index) { + Some(NodeType::Operation(packed)) => { + let op_name = packed.op.name(); + self.decrement_op(op_name); + } + _ => panic!("Must be called with valid operation node!"), + } + } + + /// Returns an iterator of the ancestors indices of a node. + pub fn ancestors(&self, node: NodeIndex) -> impl Iterator + '_ { + core_ancestors(&self.dag, node).filter(move |next| next != &node) + } + + /// Returns an iterator of the descendants of a node as DAGOpNodes and DAGOutNodes. + pub fn descendants(&self, node: NodeIndex) -> impl Iterator + '_ { + core_descendants(&self.dag, node).filter(move |next| next != &node) + } + + /// Returns an iterator of tuples of (DAGNode, [DAGNodes]) where the DAGNode is the current node + /// and [DAGNode] is its successors in BFS order. + pub fn bfs_successors( + &self, + node: NodeIndex, + ) -> impl Iterator)> + '_ { + core_bfs_successors(&self.dag, node).filter(move |(_, others)| !others.is_empty()) + } + + fn pack_into(&mut self, py: Python, b: &Bound) -> Result { + Ok(if let Ok(in_node) = b.downcast::() { + let in_node = in_node.borrow(); + let wire = in_node.wire.bind(py); + if wire.is_instance(imports::QUBIT.get_bound(py))? { + NodeType::QubitIn(self.qubits.find(wire).unwrap()) + } else if wire.is_instance(imports::CLBIT.get_bound(py))? { + NodeType::ClbitIn(self.clbits.find(wire).unwrap()) + } else { + NodeType::VarIn(wire.clone().unbind()) + } + } else if let Ok(out_node) = b.downcast::() { + let out_node = out_node.borrow(); + let wire = out_node.wire.bind(py); + if wire.is_instance(imports::QUBIT.get_bound(py))? { + NodeType::QubitOut(self.qubits.find(wire).unwrap()) + } else if wire.is_instance(imports::CLBIT.get_bound(py))? { + NodeType::ClbitOut(self.clbits.find(wire).unwrap()) + } else { + NodeType::VarIn(wire.clone().unbind()) + } + } else if let Ok(op_node) = b.downcast::() { + let op_node = op_node.borrow(); + let qubits = Interner::intern( + &mut self.qargs_cache, + self.qubits + .map_bits(op_node.instruction.qubits.bind(py))? + .collect(), + )?; + let clbits = Interner::intern( + &mut self.cargs_cache, + self.clbits + .map_bits(op_node.instruction.clbits.bind(py))? + .collect(), + )?; + let params = (!op_node.instruction.params.is_empty()) + .then(|| Box::new(op_node.instruction.params.clone())); + let inst = PackedInstruction { + op: op_node.instruction.operation.clone(), + qubits, + clbits, + params, + extra_attrs: op_node.instruction.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: op_node.instruction.py_op.clone(), + }; + NodeType::Operation(inst) + } else { + return Err(PyTypeError::new_err("Invalid type for DAGNode")); + }) + } + + fn unpack_into(&self, py: Python, id: NodeIndex, weight: &NodeType) -> PyResult> { + let dag_node = match weight { + NodeType::QubitIn(qubit) => Py::new( + py, + DAGInNode::new(py, id, self.qubits.get(*qubit).unwrap().clone_ref(py)), + )? + .into_any(), + NodeType::QubitOut(qubit) => Py::new( + py, + DAGOutNode::new(py, id, self.qubits.get(*qubit).unwrap().clone_ref(py)), + )? + .into_any(), + NodeType::ClbitIn(clbit) => Py::new( + py, + DAGInNode::new(py, id, self.clbits.get(*clbit).unwrap().clone_ref(py)), + )? + .into_any(), + NodeType::ClbitOut(clbit) => Py::new( + py, + DAGOutNode::new(py, id, self.clbits.get(*clbit).unwrap().clone_ref(py)), + )? + .into_any(), + NodeType::Operation(packed) => { + let qubits = self.qargs_cache.intern(packed.qubits); + let clbits = self.cargs_cache.intern(packed.clbits); + Py::new( + py, + ( + DAGOpNode { + instruction: CircuitInstruction { + operation: packed.op.clone(), + qubits: PyTuple::new_bound(py, self.qubits.map_indices(qubits)) + .unbind(), + clbits: PyTuple::new_bound(py, self.clbits.map_indices(clbits)) + .unbind(), + params: packed.params_view().iter().cloned().collect(), + extra_attrs: packed.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: packed.py_op.clone(), + }, + sort_key: format!("{:?}", self.sort_key(id)).into_py(py), + }, + DAGNode { node: Some(id) }, + ), + )? + .into_any() + } + NodeType::VarIn(var) => { + Py::new(py, DAGInNode::new(py, id, var.clone_ref(py)))?.into_any() + } + NodeType::VarOut(var) => { + Py::new(py, DAGOutNode::new(py, id, var.clone_ref(py)))?.into_any() + } + }; + Ok(dag_node) + } + + /// Returns an iterator over all the indices that refer to an `Operation` node in the `DAGCircuit.` + pub fn op_nodes<'a>( + &'a self, + include_directives: bool, + ) -> Box + 'a> { + let node_ops_iter = self + .dag + .node_references() + .filter_map(|(node_index, node_type)| match node_type { + NodeType::Operation(ref node) => Some((node_index, node)), + _ => None, + }); + if !include_directives { + Box::new(node_ops_iter.filter_map(|(index, node)| { + if !node.op.directive() { + Some(index) + } else { + None + } + })) + } else { + Box::new(node_ops_iter.map(|(index, _)| index)) + } + } + + pub fn op_nodes_by_py_type<'a>( + &'a self, + op: &'a Bound, + include_directives: bool, + ) -> impl Iterator + 'a { + self.dag + .node_references() + .filter_map(move |(node, weight)| { + if let NodeType::Operation(ref packed) = weight { + if !include_directives && packed.op.directive() { + None + } else if packed.op.py_op_is_instance(op).unwrap() { + Some(node) + } else { + None + } + } else { + None + } + }) + } + + /// Returns an iterator over a list layers of the `DAGCircuit``. + pub fn multigraph_layers(&self, py: Python) -> impl Iterator> + '_ { + let mut first_layer: Vec<_> = self.qubit_io_map.iter().map(|x| x[0]).collect(); + first_layer.extend(self.clbit_io_map.iter().map(|x| x[0])); + first_layer.extend(self.var_input_map.values(py)); + // A DAG is by definition acyclical, therefore unwrapping the layer should never fail. + layers(&self.dag, first_layer).map(|layer| match layer { + Ok(layer) => layer, + Err(_) => unreachable!("Not a DAG."), + }) + } + + /// Returns an iterator over the first layer of the `DAGCircuit``. + pub fn front_layer<'a>(&'a self, py: Python) -> Box + 'a> { + let mut graph_layers = self.multigraph_layers(py); + graph_layers.next(); + + let next_layer = graph_layers.next(); + match next_layer { + Some(layer) => Box::new(layer.into_iter().filter(|node| { + matches!(self.dag.node_weight(*node).unwrap(), NodeType::Operation(_)) + })), + None => Box::new(vec![].into_iter()), + } + } + + fn substitute_node_with_subgraph( + &mut self, + py: Python, + node: NodeIndex, + other: &DAGCircuit, + qubit_map: &HashMap, + clbit_map: &HashMap, + var_map: &Py, + ) -> PyResult> { + if self.dag.node_weight(node).is_none() { + return Err(PyIndexError::new_err(format!( + "Specified node {} is not in this graph", + node.index() + ))); + } + + // Add wire from pred to succ if no ops on mapped wire on ``other`` + for (in_dag_wire, self_wire) in qubit_map.iter() { + let [input_node, out_node] = other.qubit_io_map[in_dag_wire.0 as usize]; + if other.dag.find_edge(input_node, out_node).is_some() { + let pred = self + .dag + .edges_directed(node, Incoming) + .find(|edge| { + if let Wire::Qubit(bit) = edge.weight() { + bit == self_wire + } else { + false + } + }) + .unwrap(); + let succ = self + .dag + .edges_directed(node, Outgoing) + .find(|edge| { + if let Wire::Qubit(bit) = edge.weight() { + bit == self_wire + } else { + false + } + }) + .unwrap(); + self.dag + .add_edge(pred.source(), succ.target(), Wire::Qubit(*self_wire)); + } + } + for (in_dag_wire, self_wire) in clbit_map.iter() { + let [input_node, out_node] = other.clbit_io_map[in_dag_wire.0 as usize]; + if other.dag.find_edge(input_node, out_node).is_some() { + let pred = self + .dag + .edges_directed(node, Incoming) + .find(|edge| { + if let Wire::Clbit(bit) = edge.weight() { + bit == self_wire + } else { + false + } + }) + .unwrap(); + let succ = self + .dag + .edges_directed(node, Outgoing) + .find(|edge| { + if let Wire::Clbit(bit) = edge.weight() { + bit == self_wire + } else { + false + } + }) + .unwrap(); + self.dag + .add_edge(pred.source(), succ.target(), Wire::Clbit(*self_wire)); + } + } + + let bound_var_map = var_map.bind(py); + let node_filter = |node: NodeIndex| -> bool { + match other.dag[node] { + NodeType::Operation(_) => !other + .dag + .edges_directed(node, petgraph::Direction::Outgoing) + .any(|edge| match edge.weight() { + Wire::Qubit(qubit) => !qubit_map.contains_key(qubit), + Wire::Clbit(clbit) => !clbit_map.contains_key(clbit), + Wire::Var(var) => !bound_var_map.contains(var).unwrap(), + }), + _ => false, + } + }; + let reverse_qubit_map: HashMap = + qubit_map.iter().map(|(x, y)| (*y, *x)).collect(); + let reverse_clbit_map: HashMap = + clbit_map.iter().map(|(x, y)| (*y, *x)).collect(); + let reverse_var_map = PyDict::new_bound(py); + for (k, v) in bound_var_map.iter() { + reverse_var_map.set_item(v, k)?; + } + // Copy nodes from other to self + let mut out_map: IndexMap = + IndexMap::with_capacity_and_hasher(other.dag.node_count(), RandomState::default()); + for old_index in other.dag.node_indices() { + if !node_filter(old_index) { + continue; + } + let mut new_node = other.dag[old_index].clone(); + if let NodeType::Operation(ref mut new_inst) = new_node { + let new_qubit_indices: Vec = other + .qargs_cache + .intern(new_inst.qubits) + .iter() + .map(|old_qubit| qubit_map[old_qubit]) + .collect(); + let new_clbit_indices: Vec = other + .cargs_cache + .intern(new_inst.clbits) + .iter() + .map(|old_clbit| clbit_map[old_clbit]) + .collect(); + new_inst.qubits = Interner::intern(&mut self.qargs_cache, new_qubit_indices)?; + new_inst.clbits = Interner::intern(&mut self.cargs_cache, new_clbit_indices)?; + self.increment_op(new_inst.op.name()); + } + let new_index = self.dag.add_node(new_node); + out_map.insert(old_index, new_index); + } + // If no nodes are copied bail here since there is nothing left + // to do. + if out_map.is_empty() { + match self.dag.remove_node(node) { + Some(NodeType::Operation(packed)) => { + let op_name = packed.op.name(); + self.decrement_op(op_name); + } + _ => unreachable!("Must be called with valid operation node!"), + } + // Return a new empty map to clear allocation from out_map + return Ok(IndexMap::default()); + } + // Copy edges from other to self + for edge in other.dag.edge_references().filter(|edge| { + out_map.contains_key(&edge.target()) && out_map.contains_key(&edge.source()) + }) { + self.dag.add_edge( + out_map[&edge.source()], + out_map[&edge.target()], + match edge.weight() { + Wire::Qubit(qubit) => Wire::Qubit(qubit_map[qubit]), + Wire::Clbit(clbit) => Wire::Clbit(clbit_map[clbit]), + Wire::Var(var) => Wire::Var(bound_var_map.get_item(var)?.unwrap().unbind()), + }, + ); + } + // Add edges to/from node to nodes in other + let edges: Vec<(NodeIndex, NodeIndex, Wire)> = self + .dag + .edges_directed(node, Incoming) + .map(|x| (x.source(), x.target(), x.weight().clone())) + .collect(); + for (source, _target, weight) in edges { + let wire_input_id = match weight { + Wire::Qubit(qubit) => other + .qubit_io_map + .get(reverse_qubit_map[&qubit].0 as usize) + .map(|x| x[0]), + Wire::Clbit(clbit) => other + .clbit_io_map + .get(reverse_clbit_map[&clbit].0 as usize) + .map(|x| x[0]), + Wire::Var(ref var) => { + let index = &reverse_var_map.get_item(var)?.unwrap().unbind(); + other.var_input_map.get(py, index) + } + }; + let old_index = + wire_input_id.and_then(|x| other.dag.neighbors_directed(x, Outgoing).next()); + let target_out = match old_index { + Some(old_index) => match out_map.get(&old_index) { + Some(new_index) => *new_index, + None => { + // If the index isn't in the node map we've already added the edges as + // part of the idle wire handling at the top of this method so just + // move on. + continue; + } + }, + None => continue, + }; + self.dag.add_edge(source, target_out, weight); + } + let edges: Vec<(NodeIndex, NodeIndex, Wire)> = self + .dag + .edges_directed(node, Outgoing) + .map(|x| (x.source(), x.target(), x.weight().clone())) + .collect(); + for (_source, target, weight) in edges { + let wire_output_id = match weight { + Wire::Qubit(qubit) => other + .qubit_io_map + .get(reverse_qubit_map[&qubit].0 as usize) + .map(|x| x[1]), + Wire::Clbit(clbit) => other + .clbit_io_map + .get(reverse_clbit_map[&clbit].0 as usize) + .map(|x| x[1]), + Wire::Var(ref var) => { + let index = &reverse_var_map.get_item(var)?.unwrap().unbind(); + other.var_output_map.get(py, index) + } + }; + let old_index = + wire_output_id.and_then(|x| other.dag.neighbors_directed(x, Incoming).next()); + let source_out = match old_index { + Some(old_index) => match out_map.get(&old_index) { + Some(new_index) => *new_index, + None => { + // If the index isn't in the node map we've already added the edges as + // part of the idle wire handling at the top of this method so just + // move on. + continue; + } + }, + None => continue, + }; + self.dag.add_edge(source_out, target, weight); + } + // Remove node + if let NodeType::Operation(inst) = &self.dag[node] { + self.decrement_op(inst.op.name().to_string().as_str()); + } + self.dag.remove_node(node); + Ok(out_map) + } + + fn add_var(&mut self, py: Python, var: &Bound, type_: DAGVarType) -> PyResult<()> { + // The setup of the initial graph structure between an "in" and an "out" node is the same as + // the bit-related `_add_wire`, but this logically needs to do different bookkeeping around + // tracking the properties + if !var.getattr("standalone")?.extract::()? { + return Err(DAGCircuitError::new_err( + "cannot add variables that wrap `Clbit` or `ClassicalRegister` instances", + )); + } + let var_name: String = var.getattr("name")?.extract::()?; + if let Some(previous) = self.vars_info.get(&var_name) { + if var.eq(previous.var.clone_ref(py))? { + return Err(DAGCircuitError::new_err("already present in the circuit")); + } + return Err(DAGCircuitError::new_err( + "cannot add var as its name shadows an existing var", + )); + } + let in_node = NodeType::VarIn(var.clone().unbind()); + let out_node = NodeType::VarOut(var.clone().unbind()); + let in_index = self.dag.add_node(in_node); + let out_index = self.dag.add_node(out_node); + self.dag + .add_edge(in_index, out_index, Wire::Var(var.clone().unbind())); + self.var_input_map + .insert(py, var.clone().unbind(), in_index); + self.var_output_map + .insert(py, var.clone().unbind(), out_index); + self.vars_by_type[type_ as usize] + .bind(py) + .add(var.clone().unbind())?; + self.vars_info.insert( + var_name, + DAGVarInfo { + var: var.clone().unbind(), + type_, + in_node: in_index, + out_node: out_index, + }, + ); + Ok(()) + } + + fn check_op_addition(&self, py: Python, inst: &PackedInstruction) -> PyResult<()> { + if let Some(condition) = inst.condition() { + self._check_condition(py, inst.op.name(), condition.bind(py))?; + } + + for b in self.qargs_cache.intern(inst.qubits) { + if self.qubit_io_map.len() - 1 < b.0 as usize { + return Err(DAGCircuitError::new_err(format!( + "qubit {} not found in output map", + self.qubits.get(*b).unwrap() + ))); + } + } + + for b in self.cargs_cache.intern(inst.clbits) { + if !self.clbit_io_map.len() - 1 < b.0 as usize { + return Err(DAGCircuitError::new_err(format!( + "clbit {} not found in output map", + self.clbits.get(*b).unwrap() + ))); + } + } + + if self.may_have_additional_wires(py, inst) { + let (clbits, vars) = self.additional_wires(py, inst.op.view(), inst.condition())?; + for b in clbits { + if !self.clbit_io_map.len() - 1 < b.0 as usize { + return Err(DAGCircuitError::new_err(format!( + "clbit {} not found in output map", + self.clbits.get(b).unwrap() + ))); + } + } + for v in vars { + if !self.var_output_map.contains_key(py, &v) { + return Err(DAGCircuitError::new_err(format!( + "var {} not found in output map", + v + ))); + } + } + } + Ok(()) + } +} + +/// Add to global phase. Global phase can only be Float or ParameterExpression so this +/// does not handle the full possibility of parameter values. +fn add_global_phase(py: Python, phase: &Param, other: &Param) -> PyResult { + Ok(match [phase, other] { + [Param::Float(a), Param::Float(b)] => Param::Float(a + b), + [Param::Float(a), Param::ParameterExpression(b)] => Param::ParameterExpression( + b.clone_ref(py) + .call_method1(py, intern!(py, "__radd__"), (*a,))?, + ), + [Param::ParameterExpression(a), Param::Float(b)] => Param::ParameterExpression( + a.clone_ref(py) + .call_method1(py, intern!(py, "__add__"), (*b,))?, + ), + [Param::ParameterExpression(a), Param::ParameterExpression(b)] => { + Param::ParameterExpression(a.clone_ref(py).call_method1( + py, + intern!(py, "__add__"), + (b,), + )?) + } + _ => panic!("Invalid global phase"), + }) +} + +type SortKeyType<'a> = (&'a [Qubit], &'a [Clbit]); diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index 9af82b74fa5a..ccae7a8c5d82 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -12,47 +12,89 @@ #[cfg(feature = "cache_pygates")] use std::cell::OnceCell; +use std::hash::Hasher; use crate::circuit_instruction::{CircuitInstruction, OperationFromPython}; use crate::imports::QUANTUM_CIRCUIT; -use crate::operations::Operation; +use crate::operations::{Operation, Param}; +use crate::TupleLikeArg; -use numpy::IntoPyArray; +use ahash::AHasher; +use approx::relative_eq; +use rustworkx_core::petgraph::stable_graph::NodeIndex; +use numpy::IntoPyArray; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple}; +use pyo3::types::{PyString, PyTuple}; use pyo3::{intern, IntoPy, PyObject, PyResult, ToPyObject}; /// Parent class for DAGOpNode, DAGInNode, and DAGOutNode. #[pyclass(module = "qiskit._accelerate.circuit", subclass)] #[derive(Clone, Debug)] pub struct DAGNode { - #[pyo3(get, set)] - pub _node_id: isize, + pub node: Option, +} + +impl DAGNode { + #[inline] + pub fn py_nid(&self) -> isize { + self.node + .map(|node| node.index().try_into().unwrap()) + .unwrap_or(-1) + } } #[pymethods] impl DAGNode { #[new] #[pyo3(signature=(nid=-1))] - fn new(nid: isize) -> Self { - DAGNode { _node_id: nid } + fn py_new(nid: isize) -> PyResult { + Ok(DAGNode { + node: match nid { + -1 => None, + nid => { + let index: usize = match nid.try_into() { + Ok(index) => index, + Err(_) => { + return Err(PyValueError::new_err( + "Invalid node index, must be -1 or a non-negative integer", + )) + } + }; + Some(NodeIndex::new(index)) + } + }, + }) } - fn __getstate__(&self) -> isize { - self._node_id + #[getter(_node_id)] + fn get_py_node_id(&self) -> isize { + self.py_nid() } - fn __setstate__(&mut self, nid: isize) { - self._node_id = nid; + #[setter(_node_id)] + fn set_py_node_id(&mut self, nid: isize) { + self.node = match nid { + -1 => None, + nid => Some(NodeIndex::new(nid.try_into().unwrap())), + } + } + + fn __getstate__(&self) -> Option { + self.node.map(|node| node.index()) + } + + fn __setstate__(&mut self, index: Option) { + self.node = index.map(NodeIndex::new); } fn __lt__(&self, other: &DAGNode) -> bool { - self._node_id < other._node_id + self.py_nid() < other.py_nid() } fn __gt__(&self, other: &DAGNode) -> bool { - self._node_id > other._node_id + self.py_nid() > other.py_nid() } fn __str__(_self: &Bound) -> String { @@ -60,7 +102,7 @@ impl DAGNode { } fn __hash__(&self, py: Python) -> PyResult { - self._node_id.into_py(py).bind(py).hash() + self.py_nid().into_py(py).bind(py).hash() } } @@ -76,96 +118,124 @@ pub struct DAGOpNode { impl DAGOpNode { #[new] #[pyo3(signature = (op, qargs=None, cargs=None, *, dag=None))] - fn new( + pub fn py_new( py: Python, - op: &Bound, - qargs: Option<&Bound>, - cargs: Option<&Bound>, - dag: Option<&Bound>, - ) -> PyResult<(Self, DAGNode)> { - let qargs = - qargs.map_or_else(|| Ok(PyTuple::empty_bound(py)), PySequenceMethods::to_tuple)?; - let cargs = - cargs.map_or_else(|| Ok(PyTuple::empty_bound(py)), PySequenceMethods::to_tuple)?; - - let sort_key = match dag { - Some(dag) => { - let cache = dag - .getattr(intern!(py, "_key_cache"))? - .downcast_into_exact::()?; - let cache_key = PyTuple::new_bound(py, [&qargs, &cargs]); - match cache.get_item(&cache_key)? { - Some(key) => key, - None => { - let indices: PyResult> = qargs - .iter() - .chain(cargs.iter()) - .map(|bit| { - dag.call_method1(intern!(py, "find_bit"), (bit,))? - .getattr(intern!(py, "index")) - }) - .collect(); - let index_strs: Vec<_> = - indices?.into_iter().map(|i| format!("{:04}", i)).collect(); - let key = PyString::new_bound(py, index_strs.join(",").as_str()); - cache.set_item(&cache_key, &key)?; - key.into_any() + op: Bound, + qargs: Option, + cargs: Option, + #[allow(unused_variables)] dag: Option>, + ) -> PyResult> { + let py_op = op.extract::()?; + let qargs = qargs.map_or_else(|| PyTuple::empty_bound(py), |q| q.value); + let sort_key = qargs.str().unwrap().into(); + let cargs = cargs.map_or_else(|| PyTuple::empty_bound(py), |c| c.value); + let instruction = CircuitInstruction { + operation: py_op.operation, + qubits: qargs.unbind(), + clbits: cargs.unbind(), + params: py_op.params, + extra_attrs: py_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: op.unbind().into(), + }; + + Py::new( + py, + ( + DAGOpNode { + instruction, + sort_key, + }, + DAGNode { node: None }, + ), + ) + } + + fn __hash__(slf: PyRef<'_, Self>) -> PyResult { + let super_ = slf.as_ref(); + let mut hasher = AHasher::default(); + hasher.write_isize(super_.py_nid()); + hasher.write(slf.instruction.operation.name().as_bytes()); + Ok(hasher.finish()) + } + + fn __eq__(slf: PyRef, py: Python, other: &Bound) -> PyResult { + // This check is more restrictive by design as it's intended to replace + // object identitity for set/dict membership and not be a semantic equivalence + // check. We have an implementation of that as part of `DAGCircuit.__eq__` and + // this method is specifically to ensure nodes are the same. This means things + // like parameter equality are stricter to reject things like + // Param::Float(0.1) == Param::ParameterExpression(0.1) (if the expression was + // a python parameter equivalent to a bound value). + let Ok(other) = other.downcast::() else { + return Ok(false); + }; + let borrowed_other = other.borrow(); + let other_super = borrowed_other.as_ref(); + let super_ = slf.as_ref(); + + if super_.py_nid() != other_super.py_nid() { + return Ok(false); + } + if !slf + .instruction + .operation + .py_eq(py, &borrowed_other.instruction.operation)? + { + return Ok(false); + } + let params_eq = if slf.instruction.operation.try_standard_gate().is_some() { + let mut params_eq = true; + for (a, b) in slf + .instruction + .params + .iter() + .zip(borrowed_other.instruction.params.iter()) + { + let res = match [a, b] { + [Param::Float(float_a), Param::Float(float_b)] => { + relative_eq!(float_a, float_b, max_relative = 1e-10) + } + [Param::ParameterExpression(param_a), Param::ParameterExpression(param_b)] => { + param_a.bind(py).eq(param_b)? } + [Param::Obj(param_a), Param::Obj(param_b)] => param_a.bind(py).eq(param_b)?, + _ => false, + }; + if !res { + params_eq = false; + break; } } - None => qargs.str()?.into_any(), + params_eq + } else { + // We've already evaluated the parameters are equal here via the Python space equality + // check so if we're not comparing standard gates and we've reached this point we know + // the parameters are already equal. + true }; - Ok(( - DAGOpNode { - instruction: CircuitInstruction::py_new( - op, - Some(qargs.into_any()), - Some(cargs.into_any()), - )?, - sort_key: sort_key.unbind(), - }, - DAGNode { _node_id: -1 }, - )) + + Ok(params_eq + && slf + .instruction + .qubits + .bind(py) + .eq(borrowed_other.instruction.qubits.clone_ref(py))? + && slf + .instruction + .clbits + .bind(py) + .eq(borrowed_other.instruction.clbits.clone_ref(py))?) } - #[pyo3(signature = (instruction, /, *, dag=None, deepcopy=false))] + #[pyo3(signature = (instruction, /, *, deepcopy=false))] #[staticmethod] fn from_instruction( py: Python, mut instruction: CircuitInstruction, - dag: Option<&Bound>, deepcopy: bool, ) -> PyResult { - let qargs = instruction.qubits.bind(py); - let cargs = instruction.clbits.bind(py); - - let sort_key = match dag { - Some(dag) => { - let cache = dag - .getattr(intern!(py, "_key_cache"))? - .downcast_into_exact::()?; - let cache_key = PyTuple::new_bound(py, [&qargs, &cargs]); - match cache.get_item(&cache_key)? { - Some(key) => key, - None => { - let indices: PyResult> = qargs - .iter() - .chain(cargs.iter()) - .map(|bit| { - dag.call_method1(intern!(py, "find_bit"), (bit,))? - .getattr(intern!(py, "index")) - }) - .collect(); - let index_strs: Vec<_> = - indices?.into_iter().map(|i| format!("{:04}", i)).collect(); - let key = PyString::new_bound(py, index_strs.join(",").as_str()); - cache.set_item(&cache_key, &key)?; - key.into_any() - } - } - } - None => qargs.str()?.into_any(), - }; + let sort_key = instruction.qubits.bind(py).str().unwrap().into(); if deepcopy { instruction.operation = instruction.operation.py_deepcopy(py, None)?; #[cfg(feature = "cache_pygates")] @@ -173,17 +243,16 @@ impl DAGOpNode { instruction.py_op = OnceCell::new(); } } - let base = PyClassInitializer::from(DAGNode { _node_id: -1 }); + let base = PyClassInitializer::from(DAGNode { node: None }); let sub = base.add_subclass(DAGOpNode { instruction, - sort_key: sort_key.unbind(), + sort_key, }); Ok(Py::new(py, sub)?.to_object(py)) } - fn __reduce__(slf: PyRef) -> PyResult { - let py = slf.py(); - let state = (slf.as_ref()._node_id, &slf.sort_key); + fn __reduce__(slf: PyRef, py: Python) -> PyResult { + let state = (slf.as_ref().node.map(|node| node.index()), &slf.sort_key); Ok(( py.get_type_bound::(), ( @@ -197,8 +266,8 @@ impl DAGOpNode { } fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { - let (nid, sort_key): (isize, PyObject) = state.extract()?; - slf.as_mut()._node_id = nid; + let (index, sort_key): (Option, PyObject) = state.extract()?; + slf.as_mut().node = index.map(NodeIndex::new); slf.sort_key = sort_key; Ok(()) } @@ -247,16 +316,16 @@ impl DAGOpNode { #[getter] fn num_qubits(&self) -> u32 { - self.instruction.op().num_qubits() + self.instruction.operation.num_qubits() } #[getter] fn num_clbits(&self) -> u32 { - self.instruction.op().num_clbits() + self.instruction.operation.num_clbits() } #[getter] - fn get_qargs(&self, py: Python) -> Py { + pub fn get_qargs(&self, py: Python) -> Py { self.instruction.qubits.clone_ref(py) } @@ -266,7 +335,7 @@ impl DAGOpNode { } #[getter] - fn get_cargs(&self, py: Python) -> Py { + pub fn get_cargs(&self, py: Python) -> Py { self.instruction.clbits.clone_ref(py) } @@ -278,7 +347,7 @@ impl DAGOpNode { /// Returns the Instruction name corresponding to the op for this node #[getter] fn get_name(&self, py: Python) -> Py { - self.instruction.op().name().into_py(py) + self.instruction.operation.name().into_py(py) } #[getter] @@ -293,7 +362,7 @@ impl DAGOpNode { #[getter] fn matrix(&self, py: Python) -> Option { - let matrix = self.instruction.op().matrix(&self.instruction.params); + let matrix = self.instruction.operation.matrix(&self.instruction.params); matrix.map(|mat| mat.into_pyarray_bound(py).into()) } @@ -386,7 +455,7 @@ impl DAGOpNode { #[getter] fn definition<'py>(&self, py: Python<'py>) -> PyResult>> { self.instruction - .op() + .operation .definition(&self.instruction.params) .map(|data| { QUANTUM_CIRCUIT @@ -420,36 +489,69 @@ impl DAGOpNode { #[pyclass(module = "qiskit._accelerate.circuit", extends=DAGNode)] pub struct DAGInNode { #[pyo3(get)] - wire: PyObject, + pub wire: PyObject, #[pyo3(get)] sort_key: PyObject, } +impl DAGInNode { + pub fn new(py: Python, node: NodeIndex, wire: PyObject) -> (Self, DAGNode) { + ( + DAGInNode { + wire, + sort_key: intern!(py, "[]").clone().into(), + }, + DAGNode { node: Some(node) }, + ) + } +} + #[pymethods] impl DAGInNode { #[new] - fn new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { + fn py_new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { Ok(( DAGInNode { wire, - sort_key: PyList::empty_bound(py).str()?.into_any().unbind(), + sort_key: intern!(py, "[]").clone().into(), }, - DAGNode { _node_id: -1 }, + DAGNode { node: None }, )) } fn __reduce__(slf: PyRef, py: Python) -> PyObject { - let state = (slf.as_ref()._node_id, &slf.sort_key); + let state = (slf.as_ref().node.map(|node| node.index()), &slf.sort_key); (py.get_type_bound::(), (&slf.wire,), state).into_py(py) } fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { - let (nid, sort_key): (isize, PyObject) = state.extract()?; - slf.as_mut()._node_id = nid; + let (index, sort_key): (Option, PyObject) = state.extract()?; + slf.as_mut().node = index.map(NodeIndex::new); slf.sort_key = sort_key; Ok(()) } + fn __hash__(slf: PyRef<'_, Self>, py: Python) -> PyResult { + let super_ = slf.as_ref(); + let mut hasher = AHasher::default(); + hasher.write_isize(super_.py_nid()); + hasher.write_isize(slf.wire.bind(py).hash()?); + Ok(hasher.finish()) + } + + fn __eq__(slf: PyRef, py: Python, other: &Bound) -> PyResult { + match other.downcast::() { + Ok(other) => { + let borrowed_other = other.borrow(); + let other_super = borrowed_other.as_ref(); + let super_ = slf.as_ref(); + Ok(super_.py_nid() == other_super.py_nid() + && slf.wire.bind(py).eq(borrowed_other.wire.clone_ref(py))?) + } + Err(_) => Ok(false), + } + } + /// Returns a representation of the DAGInNode fn __repr__(&self, py: Python) -> PyResult { Ok(format!("DAGInNode(wire={})", self.wire.bind(py).repr()?)) @@ -460,38 +562,71 @@ impl DAGInNode { #[pyclass(module = "qiskit._accelerate.circuit", extends=DAGNode)] pub struct DAGOutNode { #[pyo3(get)] - wire: PyObject, + pub wire: PyObject, #[pyo3(get)] sort_key: PyObject, } +impl DAGOutNode { + pub fn new(py: Python, node: NodeIndex, wire: PyObject) -> (Self, DAGNode) { + ( + DAGOutNode { + wire, + sort_key: intern!(py, "[]").clone().into(), + }, + DAGNode { node: Some(node) }, + ) + } +} + #[pymethods] impl DAGOutNode { #[new] - fn new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { + fn py_new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { Ok(( DAGOutNode { wire, - sort_key: PyList::empty_bound(py).str()?.into_any().unbind(), + sort_key: intern!(py, "[]").clone().into(), }, - DAGNode { _node_id: -1 }, + DAGNode { node: None }, )) } fn __reduce__(slf: PyRef, py: Python) -> PyObject { - let state = (slf.as_ref()._node_id, &slf.sort_key); + let state = (slf.as_ref().node.map(|node| node.index()), &slf.sort_key); (py.get_type_bound::(), (&slf.wire,), state).into_py(py) } fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { - let (nid, sort_key): (isize, PyObject) = state.extract()?; - slf.as_mut()._node_id = nid; + let (index, sort_key): (Option, PyObject) = state.extract()?; + slf.as_mut().node = index.map(NodeIndex::new); slf.sort_key = sort_key; Ok(()) } + fn __hash__(slf: PyRef<'_, Self>, py: Python) -> PyResult { + let super_ = slf.as_ref(); + let mut hasher = AHasher::default(); + hasher.write_isize(super_.py_nid()); + hasher.write_isize(slf.wire.bind(py).hash()?); + Ok(hasher.finish()) + } + /// Returns a representation of the DAGOutNode fn __repr__(&self, py: Python) -> PyResult { Ok(format!("DAGOutNode(wire={})", self.wire.bind(py).repr()?)) } + + fn __eq__(slf: PyRef, py: Python, other: &Bound) -> PyResult { + match other.downcast::() { + Ok(other) => { + let borrowed_other = other.borrow(); + let other_super = borrowed_other.as_ref(); + let super_ = slf.as_ref(); + Ok(super_.py_nid() == other_super.py_nid() + && slf.wire.bind(py).eq(borrowed_other.wire.clone_ref(py))?) + } + Err(_) => Ok(false), + } + } } diff --git a/crates/circuit/src/dot_utils.rs b/crates/circuit/src/dot_utils.rs new file mode 100644 index 000000000000..f6769c6cad11 --- /dev/null +++ b/crates/circuit/src/dot_utils.rs @@ -0,0 +1,109 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2022 +// +// 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. + +// This module is forked from rustworkx at: +// https://github.com/Qiskit/rustworkx/blob/c4256daf96fc3c08c392450ed33bc0987cdb15ff/src/dot_utils.rs +// and has been modified to generate a dot file from a Rust DAGCircuit instead +// of a rustworkx PyGraph object + +use std::collections::BTreeMap; +use std::io::prelude::*; + +use crate::dag_circuit::{DAGCircuit, Wire}; +use pyo3::prelude::*; +use rustworkx_core::petgraph::visit::{ + EdgeRef, IntoEdgeReferences, IntoNodeReferences, NodeIndexable, NodeRef, +}; + +static TYPE: [&str; 2] = ["graph", "digraph"]; +static EDGE: [&str; 2] = ["--", "->"]; + +pub fn build_dot( + py: Python, + dag: &DAGCircuit, + file: &mut T, + graph_attrs: Option>, + node_attrs: Option, + edge_attrs: Option, +) -> PyResult<()> +where + T: Write, +{ + let graph = &dag.dag; + writeln!(file, "{} {{", TYPE[graph.is_directed() as usize])?; + if let Some(graph_attr_map) = graph_attrs { + for (key, value) in graph_attr_map.iter() { + writeln!(file, "{}={} ;", key, value)?; + } + } + + for node in graph.node_references() { + let node_weight = dag.get_node(py, node.id())?; + writeln!( + file, + "{} {};", + graph.to_index(node.id()), + attr_map_to_string(py, node_attrs.as_ref(), node_weight)? + )?; + } + for edge in graph.edge_references() { + let edge_weight = match edge.weight() { + Wire::Qubit(qubit) => dag.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => dag.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }; + writeln!( + file, + "{} {} {} {};", + graph.to_index(edge.source()), + EDGE[graph.is_directed() as usize], + graph.to_index(edge.target()), + attr_map_to_string(py, edge_attrs.as_ref(), edge_weight)? + )?; + } + writeln!(file, "}}")?; + Ok(()) +} + +static ATTRS_TO_ESCAPE: [&str; 2] = ["label", "tooltip"]; + +/// Convert an attr map to an output string +fn attr_map_to_string( + py: Python, + attrs: Option<&PyObject>, + weight: T, +) -> PyResult { + if attrs.is_none() { + return Ok("".to_string()); + } + let attr_callable = |node: T| -> PyResult> { + let res = attrs.unwrap().call1(py, (node.to_object(py),))?; + res.extract(py) + }; + + let attrs = attr_callable(weight)?; + if attrs.is_empty() { + return Ok("".to_string()); + } + let attr_string = attrs + .iter() + .map(|(key, value)| { + if ATTRS_TO_ESCAPE.contains(&key.as_str()) { + format!("{}=\"{}\"", key, value) + } else { + format!("{}={}", key, value) + } + }) + .collect::>() + .join(", "); + Ok(format!("[{}]", attr_string)) +} diff --git a/crates/circuit/src/error.rs b/crates/circuit/src/error.rs new file mode 100644 index 000000000000..b28e6b8f513b --- /dev/null +++ b/crates/circuit/src/error.rs @@ -0,0 +1,16 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 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::import_exception; + +import_exception!(qiskit.dagcircuit.exceptions, DAGCircuitError); +import_exception!(qiskit.dagcircuit.exceptions, DAGDependencyError); diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index d9d439bb4745..588471546273 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -57,6 +57,7 @@ impl ImportOnceCell { } pub static BUILTIN_LIST: ImportOnceCell = ImportOnceCell::new("builtins", "list"); +pub static BUILTIN_SET: ImportOnceCell = ImportOnceCell::new("builtins", "set"); pub static OPERATION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.operation", "Operation"); pub static INSTRUCTION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.instruction", "Instruction"); @@ -65,6 +66,10 @@ pub static CONTROL_FLOW_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.controlflow", "ControlFlowOp"); pub static QUBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.quantumregister", "Qubit"); pub static CLBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.classicalregister", "Clbit"); +pub static QUANTUM_REGISTER: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.quantumregister", "QuantumRegister"); +pub static CLASSICAL_REGISTER: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.classicalregister", "ClassicalRegister"); pub static PARAMETER_EXPRESSION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.parameterexpression", "ParameterExpression"); pub static QUANTUM_CIRCUIT: ImportOnceCell = @@ -73,6 +78,17 @@ pub static SINGLETON_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.singleton", "SingletonGate"); pub static SINGLETON_CONTROLLED_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.singleton", "SingletonControlledGate"); +pub static VARIABLE_MAPPER: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit._classical_resource_map", "VariableMapper"); +pub static IF_ELSE_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "IfElseOp"); +pub static FOR_LOOP_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "ForLoopOp"); +pub static SWITCH_CASE_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "SwitchCaseOp"); +pub static WHILE_LOOP_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "WhileLoopOp"); +pub static STORE_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "Store"); +pub static EXPR: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.classical.expr", "Expr"); +pub static ITER_VARS: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.classical.expr", "iter_vars"); +pub static DAG_NODE: ImportOnceCell = ImportOnceCell::new("qiskit.dagcircuit", "DAGNode"); pub static CONTROLLED_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "ControlledGate"); pub static ANNOTATED_OPERATION: ImportOnceCell = @@ -80,6 +96,18 @@ pub static ANNOTATED_OPERATION: ImportOnceCell = pub static DEEPCOPY: ImportOnceCell = ImportOnceCell::new("copy", "deepcopy"); pub static QI_OPERATOR: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "Operator"); pub static WARNINGS_WARN: ImportOnceCell = ImportOnceCell::new("warnings", "warn"); +pub static CIRCUIT_TO_DAG: ImportOnceCell = + ImportOnceCell::new("qiskit.converters", "circuit_to_dag"); +pub static DAG_TO_CIRCUIT: ImportOnceCell = + ImportOnceCell::new("qiskit.converters", "dag_to_circuit"); +pub static LEGACY_CONDITION_CHECK: ImportOnceCell = + ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_legacy_condition_eq"); +pub static CONDITION_OP_CHECK: ImportOnceCell = + ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_condition_op_eq"); +pub static SWITCH_CASE_OP_CHECK: ImportOnceCell = + ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_switch_case_eq"); +pub static FOR_LOOP_OP_CHECK: ImportOnceCell = + ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_for_loop_eq"); pub static UUID: ImportOnceCell = ImportOnceCell::new("uuid", "UUID"); /// A mapping from the enum variant in crate::operations::StandardGate to the python diff --git a/crates/circuit/src/interner.rs b/crates/circuit/src/interner.rs index f22bb80ae052..e19f56e87a7d 100644 --- a/crates/circuit/src/interner.rs +++ b/crates/circuit/src/interner.rs @@ -17,39 +17,15 @@ use hashbrown::HashMap; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Index(u32); -pub enum InternerKey { - Index(Index), - Value(T), -} - -impl From for InternerKey { - fn from(value: Index) -> Self { - InternerKey::Index(value) - } -} - -pub struct InternerValue<'a, T> { - pub index: Index, - pub value: &'a T, -} - impl IntoPy for Index { fn into_py(self, py: Python<'_>) -> PyObject { self.0.into_py(py) } } -pub struct CacheFullError; - -impl From for PyErr { - fn from(_: CacheFullError) -> Self { - PyRuntimeError::new_err("The bit operands cache is full!") - } -} - /// An append-only data structure for interning generic /// Rust types. #[derive(Clone, Debug)] @@ -58,25 +34,20 @@ pub struct IndexedInterner { index_lookup: HashMap, Index>, } -pub trait Interner { - type Key; +pub trait Interner { type Output; /// Takes ownership of the provided key and returns the interned /// type. - fn intern(self, value: Self::Key) -> Self::Output; + fn intern(self, value: K) -> Self::Output; } -impl<'a, T> Interner for &'a IndexedInterner { - type Key = Index; - type Output = InternerValue<'a, T>; +impl<'a, T> Interner for &'a IndexedInterner { + type Output = &'a T; fn intern(self, index: Index) -> Self::Output { let value = self.entries.get(index.0 as usize).unwrap(); - InternerValue { - index, - value: value.as_ref(), - } + value.as_ref() } } @@ -84,35 +55,19 @@ impl<'a, T> Interner for &'a mut IndexedInterner where T: Eq + Hash, { - type Key = InternerKey; - type Output = Result, CacheFullError>; - - fn intern(self, key: Self::Key) -> Self::Output { - match key { - InternerKey::Index(index) => { - let value = self.entries.get(index.0 as usize).unwrap(); - Ok(InternerValue { - index, - value: value.as_ref(), - }) - } - InternerKey::Value(value) => { - if let Some(index) = self.index_lookup.get(&value).copied() { - Ok(InternerValue { - index, - value: self.entries.get(index.0 as usize).unwrap(), - }) - } else { - let args = Arc::new(value); - let index: Index = - Index(self.entries.len().try_into().map_err(|_| CacheFullError)?); - self.entries.push(args.clone()); - Ok(InternerValue { - index, - value: self.index_lookup.insert_unique_unchecked(args, index).0, - }) - } - } + type Output = PyResult; + + fn intern(self, key: T) -> Self::Output { + if let Some(index) = self.index_lookup.get(&key).copied() { + Ok(index) + } else { + let args = Arc::new(key); + let index: Index = Index(self.entries.len().try_into().map_err(|_| { + PyRuntimeError::new_err("The interner has run out of indices (cache is full)!") + })?); + self.entries.push(args.clone()); + self.index_lookup.insert_unique_unchecked(args, index); + Ok(index) } } } diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 739bf998a611..4ca86c2ca83c 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -13,18 +13,23 @@ pub mod bit_data; pub mod circuit_data; pub mod circuit_instruction; +pub mod dag_circuit; pub mod dag_node; +mod dot_utils; +mod error; pub mod gate_matrix; pub mod imports; +mod interner; pub mod operations; pub mod packed_instruction; pub mod parameter_table; pub mod slice; pub mod util; -mod interner; +mod rustworkx_core_vnext; use pyo3::prelude::*; +use pyo3::types::{PySequence, PyTuple}; pub type BitType = u32; #[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] @@ -32,6 +37,25 @@ pub struct Qubit(pub BitType); #[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] pub struct Clbit(pub BitType); +pub struct TupleLikeArg<'py> { + value: Bound<'py, PyTuple>, +} + +impl<'py> FromPyObject<'py> for TupleLikeArg<'py> { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + let value = match ob.downcast::() { + Ok(seq) => seq.to_tuple()?, + Err(_) => PyTuple::new_bound( + ob.py(), + ob.iter()? + .map(|o| Ok(o?.unbind())) + .collect::>>()?, + ), + }; + Ok(TupleLikeArg { value }) + } +} + impl From for Qubit { fn from(value: BitType) -> Self { Qubit(value) @@ -58,11 +82,12 @@ impl From for BitType { pub fn circuit(m: &Bound) -> PyResult<()> { m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; Ok(()) } diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 7134662e1ac6..ccf41d7eefb2 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -10,6 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use approx::relative_eq; use std::f64::consts::PI; use crate::circuit_data::CircuitData; @@ -35,6 +36,29 @@ pub enum Param { Obj(PyObject), } +impl Param { + pub fn eq(&self, py: Python, other: &Param) -> PyResult { + match [self, other] { + [Self::Float(a), Self::Float(b)] => Ok(a == b), + [Self::Float(a), Self::ParameterExpression(b)] => b.bind(py).eq(a), + [Self::ParameterExpression(a), Self::Float(b)] => a.bind(py).eq(b), + [Self::ParameterExpression(a), Self::ParameterExpression(b)] => a.bind(py).eq(b), + [Self::Obj(_), Self::Float(_)] => Ok(false), + [Self::Float(_), Self::Obj(_)] => Ok(false), + [Self::Obj(a), Self::ParameterExpression(b)] => a.bind(py).eq(b), + [Self::Obj(a), Self::Obj(b)] => a.bind(py).eq(b), + [Self::ParameterExpression(a), Self::Obj(b)] => a.bind(py).eq(b), + } + } + + pub fn is_close(&self, py: Python, other: &Param, max_relative: f64) -> PyResult { + match [self, other] { + [Self::Float(a), Self::Float(b)] => Ok(relative_eq!(a, b, max_relative = max_relative)), + _ => self.eq(py, other), + } + } +} + impl<'py> FromPyObject<'py> for Param { fn extract_bound(b: &Bound<'py, PyAny>) -> Result { Ok(if b.is_instance(PARAMETER_EXPRESSION.get_bound(b.py()))? { @@ -131,6 +155,7 @@ pub trait Operation { /// `PackedInstruction::op`, and in turn is a view object onto a `PackedOperation`. /// /// This is the main way that we interact immutably with general circuit operations from Rust space. +#[derive(Debug)] pub enum OperationRef<'a> { Standard(StandardGate), Gate(&'a PyGate), diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs index 9c4f19aa1ba3..7619ecb9c525 100644 --- a/crates/circuit/src/packed_instruction.rs +++ b/crates/circuit/src/packed_instruction.rs @@ -16,13 +16,18 @@ use std::ptr::NonNull; use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::PyDict; +use pyo3::types::{PyDict, PyType}; +use ndarray::Array2; +use num_complex::Complex64; use smallvec::SmallVec; +use crate::circuit_data::CircuitData; use crate::circuit_instruction::ExtraInstructionAttributes; -use crate::imports::DEEPCOPY; -use crate::operations::{OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate}; +use crate::imports::{get_std_gate_class, DEEPCOPY}; +use crate::operations::{ + Operation, OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate, +}; /// The logical discriminant of `PackedOperation`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -331,6 +336,78 @@ impl PackedOperation { .into()), } } + + /// Whether the Python class that we would use to represent the inner `Operation` object in + /// Python space would be an instance of the given Python type. This does not construct the + /// Python-space `Operator` instance if it can be avoided (i.e. for standard gates). + pub fn py_op_is_instance(&self, py_type: &Bound) -> PyResult { + let py = py_type.py(); + let py_op = match self.view() { + OperationRef::Standard(standard) => { + return get_std_gate_class(py, standard)? + .bind(py) + .downcast::()? + .is_subclass(py_type) + } + OperationRef::Gate(gate) => gate.gate.bind(py), + OperationRef::Instruction(instruction) => instruction.instruction.bind(py), + OperationRef::Operation(operation) => operation.operation.bind(py), + }; + py_op.is_instance(py_type) + } +} + +impl Operation for PackedOperation { + fn name(&self) -> &str { + let view = self.view(); + let name = match view { + OperationRef::Standard(ref standard) => standard.name(), + OperationRef::Gate(gate) => gate.name(), + OperationRef::Instruction(instruction) => instruction.name(), + OperationRef::Operation(operation) => operation.name(), + }; + // SAFETY: all of the inner parts of the view are owned by `self`, so it's valid for us to + // forcibly reborrowing up to our own lifetime. We avoid using `` + // just to avoid a further _potential_ unsafeness, were its implementation to start doing + // something weird with the lifetimes. `str::from_utf8_unchecked` and + // `slice::from_raw_parts` are both trivially safe because they're being called on immediate + // values from a validated `str`. + unsafe { + ::std::str::from_utf8_unchecked(::std::slice::from_raw_parts(name.as_ptr(), name.len())) + } + } + #[inline] + fn num_qubits(&self) -> u32 { + self.view().num_qubits() + } + #[inline] + fn num_clbits(&self) -> u32 { + self.view().num_clbits() + } + #[inline] + fn num_params(&self) -> u32 { + self.view().num_params() + } + #[inline] + fn control_flow(&self) -> bool { + self.view().control_flow() + } + #[inline] + fn matrix(&self, params: &[Param]) -> Option> { + self.view().matrix(params) + } + #[inline] + fn definition(&self, params: &[Param]) -> Option { + self.view().definition(params) + } + #[inline] + fn standard_gate(&self) -> Option { + self.view().standard_gate() + } + #[inline] + fn directive(&self) -> bool { + self.view().directive() + } } impl From for PackedOperation { @@ -435,15 +512,6 @@ pub struct PackedInstruction { } impl PackedInstruction { - /// Immutably view the contained operation. - /// - /// If you only care whether the contained operation is a `StandardGate` or not, you can use - /// `PackedInstruction::standard_gate`, which is a bit cheaper than this function. - #[inline] - pub fn op(&self) -> OperationRef { - self.op.view() - } - /// Access the standard gate in this `PackedInstruction`, if it is one. If the instruction /// refers to a Python-space object, `None` is returned. #[inline] @@ -469,6 +537,20 @@ impl PackedInstruction { .unwrap_or(&mut []) } + /// Does this instruction contain any compile-time symbolic `ParameterExpression`s? + pub fn is_parameterized(&self) -> bool { + self.params_view() + .iter() + .any(|x| matches!(x, Param::ParameterExpression(_))) + } + + #[inline] + pub fn condition(&self) -> Option<&Py> { + self.extra_attrs + .as_ref() + .and_then(|extra| extra.condition.as_ref()) + } + /// Build a reference to the Python-space operation object (the `Gate`, etc) packed into this /// instruction. This may construct the reference if the `PackedInstruction` is a standard /// gate with no already stored operation. @@ -510,4 +592,30 @@ impl PackedInstruction { } Ok(out) } + + /// Check equality of the operation, including Python-space checks, if appropriate. + pub fn py_op_eq(&self, py: Python, other: &Self) -> PyResult { + match (self.op.view(), other.op.view()) { + (OperationRef::Standard(left), OperationRef::Standard(right)) => Ok(left == right), + (OperationRef::Gate(left), OperationRef::Gate(right)) => { + left.gate.bind(py).eq(&right.gate) + } + (OperationRef::Instruction(left), OperationRef::Instruction(right)) => { + left.instruction.bind(py).eq(&right.instruction) + } + (OperationRef::Operation(left), OperationRef::Operation(right)) => { + left.operation.bind(py).eq(&right.operation) + } + // Handle the case we end up with a pygate for a standard gate + // this typically only happens if it's a ControlledGate in python + // and we have mutable state set. + (OperationRef::Standard(_left), OperationRef::Gate(right)) => { + self.unpack_py_op(py)?.bind(py).eq(&right.gate) + } + (OperationRef::Gate(left), OperationRef::Standard(_right)) => { + other.unpack_py_op(py)?.bind(py).eq(&left.gate) + } + _ => Ok(false), + } + } } diff --git a/crates/circuit/src/rustworkx_core_vnext.rs b/crates/circuit/src/rustworkx_core_vnext.rs new file mode 100644 index 000000000000..e69ee88a93e7 --- /dev/null +++ b/crates/circuit/src/rustworkx_core_vnext.rs @@ -0,0 +1,1417 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 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. + +// TODO: delete once we move to a version of Rustworkx which includes +// this implementation as part of rustworkx-core. +// PR: https://github.com/Qiskit/rustworkx/pull/1235 +pub mod isomorphism { + pub mod vf2 { + #![allow(clippy::too_many_arguments)] + // This module was originally forked from petgraph's isomorphism module @ v0.5.0 + // to handle PyDiGraph inputs instead of petgraph's generic Graph. However it has + // since diverged significantly from the original petgraph implementation. + + use std::cmp::{Ordering, Reverse}; + use std::convert::Infallible; + use std::error::Error; + use std::fmt::{Debug, Display, Formatter}; + use std::iter::Iterator; + use std::marker; + use std::ops::Deref; + + use hashbrown::HashMap; + use rustworkx_core::dictmap::*; + + use rustworkx_core::petgraph::data::{Build, Create, DataMap}; + use rustworkx_core::petgraph::stable_graph::NodeIndex; + use rustworkx_core::petgraph::visit::{ + Data, EdgeCount, EdgeRef, GraphBase, GraphProp, IntoEdgeReferences, IntoEdges, + IntoEdgesDirected, IntoNeighbors, IntoNeighborsDirected, IntoNodeIdentifiers, + NodeCount, NodeIndexable, + }; + use rustworkx_core::petgraph::{Directed, Incoming, Outgoing}; + + use rayon::slice::ParallelSliceMut; + + /// Returns `true` if we can map every element of `xs` to a unique + /// element of `ys` while using `matcher` func to compare two elements. + fn is_subset( + xs: &[T1], + ys: &[T2], + matcher: &mut F, + ) -> Result + where + F: FnMut(T1, T2) -> Result, + { + let mut valid = vec![true; ys.len()]; + for &a in xs { + let mut found = false; + for (&b, free) in ys.iter().zip(valid.iter_mut()) { + if *free && matcher(a, b)? { + found = true; + *free = false; + break; + } + } + + if !found { + return Ok(false); + } + } + + Ok(true) + } + + #[inline] + fn sorted(x: &mut (N, N)) { + let (a, b) = x; + if b < a { + std::mem::swap(a, b) + } + } + + /// Returns the adjacency matrix of a graph as a dictionary + /// with `(i, j)` entry equal to number of edges from node `i` to node `j`. + fn adjacency_matrix(graph: G) -> HashMap<(NodeIndex, NodeIndex), usize> + where + G: GraphProp + GraphBase + EdgeCount + IntoEdgeReferences, + { + let mut matrix = HashMap::with_capacity(graph.edge_count()); + for edge in graph.edge_references() { + let mut item = (edge.source(), edge.target()); + if !graph.is_directed() { + sorted(&mut item); + } + let entry = matrix.entry(item).or_insert(0); + *entry += 1; + } + matrix + } + + /// Returns the number of edges from node `a` to node `b`. + fn edge_multiplicity( + graph: &G, + matrix: &HashMap<(NodeIndex, NodeIndex), usize>, + a: NodeIndex, + b: NodeIndex, + ) -> usize + where + G: GraphProp + GraphBase, + { + let mut item = (a, b); + if !graph.is_directed() { + sorted(&mut item); + } + *matrix.get(&item).unwrap_or(&0) + } + + /// Nodes `a`, `b` are adjacent if the number of edges + /// from node `a` to node `b` is greater than `val`. + fn is_adjacent( + graph: &G, + matrix: &HashMap<(NodeIndex, NodeIndex), usize>, + a: NodeIndex, + b: NodeIndex, + val: usize, + ) -> bool + where + G: GraphProp + GraphBase, + { + edge_multiplicity(graph, matrix, a, b) >= val + } + + trait NodeSorter + where + G: GraphBase + DataMap + NodeCount + EdgeCount + IntoEdgeReferences, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, + { + type OutputGraph: GraphBase + + Create + + Data; + + fn sort(&self, _: G) -> Vec; + + fn reorder(&self, graph: G) -> (Self::OutputGraph, HashMap) { + let order = self.sort(graph); + + let mut new_graph = + Self::OutputGraph::with_capacity(graph.node_count(), graph.edge_count()); + let mut id_map: HashMap = + HashMap::with_capacity(graph.node_count()); + for node_index in order { + let node_data = graph.node_weight(node_index).unwrap(); + let new_index = new_graph.add_node(node_data.clone()); + id_map.insert(node_index, new_index); + } + for edge in graph.edge_references() { + let edge_w = edge.weight(); + let p_index = id_map[&edge.source()]; + let c_index = id_map[&edge.target()]; + new_graph.add_edge(p_index, c_index, edge_w.clone()); + } + ( + new_graph, + id_map.iter().map(|(k, v)| (v.index(), k.index())).collect(), + ) + } + } + + /// Sort nodes based on node ids. + struct DefaultIdSorter {} + + impl DefaultIdSorter { + pub fn new() -> Self { + Self {} + } + } + + impl NodeSorter for DefaultIdSorter + where + G: Deref + + GraphBase + + DataMap + + NodeCount + + EdgeCount + + IntoEdgeReferences + + IntoNodeIdentifiers, + G::Target: GraphBase + + Data + + Create, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, + { + type OutputGraph = G::Target; + fn sort(&self, graph: G) -> Vec { + graph.node_identifiers().collect() + } + } + + /// Sort nodes based on VF2++ heuristic. + struct Vf2ppSorter {} + + impl Vf2ppSorter { + pub fn new() -> Self { + Self {} + } + } + + impl NodeSorter for Vf2ppSorter + where + G: Deref + + GraphProp + + GraphBase + + DataMap + + NodeCount + + NodeIndexable + + EdgeCount + + IntoNodeIdentifiers + + IntoEdgesDirected, + G::Target: GraphBase + + Data + + Create, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, + { + type OutputGraph = G::Target; + fn sort(&self, graph: G) -> Vec { + let n = graph.node_bound(); + + let dout: Vec = (0..n) + .map(|idx| { + graph + .neighbors_directed(graph.from_index(idx), Outgoing) + .count() + }) + .collect(); + + let mut din: Vec = vec![0; n]; + if graph.is_directed() { + din = (0..n) + .map(|idx| { + graph + .neighbors_directed(graph.from_index(idx), Incoming) + .count() + }) + .collect(); + } + + let mut conn_in: Vec = vec![0; n]; + let mut conn_out: Vec = vec![0; n]; + + let mut order: Vec = Vec::with_capacity(n); + + // Process BFS level + let mut process = |mut vd: Vec| -> Vec { + // repeatedly bring largest element in front. + for i in 0..vd.len() { + let (index, &item) = vd[i..] + .iter() + .enumerate() + .max_by_key(|&(_, &node)| { + ( + conn_in[node], + dout[node], + conn_out[node], + din[node], + Reverse(node), + ) + }) + .unwrap(); + + vd.swap(i, i + index); + order.push(NodeIndex::new(item)); + + for neigh in graph.neighbors_directed(graph.from_index(item), Outgoing) { + conn_in[graph.to_index(neigh)] += 1; + } + + if graph.is_directed() { + for neigh in graph.neighbors_directed(graph.from_index(item), Incoming) + { + conn_out[graph.to_index(neigh)] += 1; + } + } + } + vd + }; + + let mut seen: Vec = vec![false; n]; + + // Create BFS Tree from root and process each level. + let mut bfs_tree = |root: usize| { + if seen[root] { + return; + } + + let mut next_level: Vec = Vec::new(); + + seen[root] = true; + next_level.push(root); + while !next_level.is_empty() { + let this_level = next_level; + let this_level = process(this_level); + + next_level = Vec::new(); + for bfs_node in this_level { + for neighbor in + graph.neighbors_directed(graph.from_index(bfs_node), Outgoing) + { + let neigh = graph.to_index(neighbor); + if !seen[neigh] { + seen[neigh] = true; + next_level.push(neigh); + } + } + } + } + }; + + let mut sorted_nodes: Vec = + graph.node_identifiers().map(|node| node.index()).collect(); + sorted_nodes.par_sort_by_key(|&node| (dout[node], din[node], Reverse(node))); + sorted_nodes.reverse(); + + for node in sorted_nodes { + bfs_tree(node); + } + + order + } + } + + #[derive(Debug)] + pub struct Vf2State { + pub graph: G, + /// The current mapping M(s) of nodes from G0 → G1 and G1 → G0, + /// NodeIndex::end() for no mapping. + mapping: Vec, + /// out[i] is non-zero if i is in either M_0(s) or Tout_0(s) + /// These are all the next vertices that are not mapped yet, but + /// have an outgoing edge from the mapping. + out: Vec, + /// ins[i] is non-zero if i is in either M_0(s) or Tin_0(s) + /// These are all the incoming vertices, those not mapped yet, but + /// have an edge from them into the mapping. + /// Unused if graph is undirected -- it's identical with out in that case. + ins: Vec, + out_size: usize, + ins_size: usize, + adjacency_matrix: HashMap<(NodeIndex, NodeIndex), usize>, + generation: usize, + _etype: marker::PhantomData, + } + + impl Vf2State + where + G: GraphBase + GraphProp + NodeCount + EdgeCount, + for<'a> &'a G: GraphBase + + GraphProp + + NodeCount + + EdgeCount + + IntoEdgesDirected, + { + pub fn new(graph: G) -> Self { + let c0 = graph.node_count(); + let is_directed = graph.is_directed(); + let adjacency_matrix = adjacency_matrix(&graph); + Vf2State { + graph, + mapping: vec![NodeIndex::end(); c0], + out: vec![0; c0], + ins: vec![0; c0 * (is_directed as usize)], + out_size: 0, + ins_size: 0, + adjacency_matrix, + generation: 0, + _etype: marker::PhantomData, + } + } + + /// Return **true** if we have a complete mapping + pub fn is_complete(&self) -> bool { + self.generation == self.mapping.len() + } + + /// Add mapping **from** <-> **to** to the state. + pub fn push_mapping(&mut self, from: NodeIndex, to: NodeIndex) { + self.generation += 1; + let s = self.generation; + self.mapping[from.index()] = to; + // update T0 & T1 ins/outs + // T0out: Node in G0 not in M0 but successor of a node in M0. + // st.out[0]: Node either in M0 or successor of M0 + for ix in self.graph.neighbors(from) { + if self.out[ix.index()] == 0 { + self.out[ix.index()] = s; + self.out_size += 1; + } + } + if self.graph.is_directed() { + for ix in self.graph.neighbors_directed(from, Incoming) { + if self.ins[ix.index()] == 0 { + self.ins[ix.index()] = s; + self.ins_size += 1; + } + } + } + } + + /// Restore the state to before the last added mapping + pub fn pop_mapping(&mut self, from: NodeIndex) { + let s = self.generation; + self.generation -= 1; + + // undo (n, m) mapping + self.mapping[from.index()] = NodeIndex::end(); + + // unmark in ins and outs + for ix in self.graph.neighbors(from) { + if self.out[ix.index()] == s { + self.out[ix.index()] = 0; + self.out_size -= 1; + } + } + if self.graph.is_directed() { + for ix in self.graph.neighbors_directed(from, Incoming) { + if self.ins[ix.index()] == s { + self.ins[ix.index()] = 0; + self.ins_size -= 1; + } + } + } + } + + /// Find the next (least) node in the Tout set. + pub fn next_out_index(&self, from_index: usize) -> Option { + self.out[from_index..] + .iter() + .enumerate() + .find(move |&(index, elt)| { + *elt > 0 && self.mapping[from_index + index] == NodeIndex::end() + }) + .map(|(index, _)| index) + } + + /// Find the next (least) node in the Tin set. + pub fn next_in_index(&self, from_index: usize) -> Option { + self.ins[from_index..] + .iter() + .enumerate() + .find(move |&(index, elt)| { + *elt > 0 && self.mapping[from_index + index] == NodeIndex::end() + }) + .map(|(index, _)| index) + } + + /// Find the next (least) node in the N - M set. + pub fn next_rest_index(&self, from_index: usize) -> Option { + self.mapping[from_index..] + .iter() + .enumerate() + .find(|&(_, elt)| *elt == NodeIndex::end()) + .map(|(index, _)| index) + } + } + + #[derive(Debug)] + pub enum IsIsomorphicError { + NodeMatcherErr(NME), + EdgeMatcherErr(EME), + } + + impl Display for IsIsomorphicError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + IsIsomorphicError::NodeMatcherErr(e) => { + write!(f, "Node match callback failed with: {}", e) + } + IsIsomorphicError::EdgeMatcherErr(e) => { + write!(f, "Edge match callback failed with: {}", e) + } + } + } + } + + impl Error for IsIsomorphicError {} + + pub struct NoSemanticMatch; + + pub trait NodeMatcher { + type Error; + fn enabled(&self) -> bool; + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + _n0: G0::NodeId, + _n1: G1::NodeId, + ) -> Result; + } + + impl NodeMatcher for NoSemanticMatch { + type Error = Infallible; + #[inline] + fn enabled(&self) -> bool { + false + } + #[inline] + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + _n0: G0::NodeId, + _n1: G1::NodeId, + ) -> Result { + Ok(true) + } + } + + impl NodeMatcher for F + where + G0: GraphBase + DataMap, + G1: GraphBase + DataMap, + F: FnMut(&G0::NodeWeight, &G1::NodeWeight) -> Result, + { + type Error = E; + #[inline] + fn enabled(&self) -> bool { + true + } + #[inline] + fn eq( + &mut self, + g0: &G0, + g1: &G1, + n0: G0::NodeId, + n1: G1::NodeId, + ) -> Result { + if let (Some(x), Some(y)) = (g0.node_weight(n0), g1.node_weight(n1)) { + self(x, y) + } else { + Ok(false) + } + } + } + + pub trait EdgeMatcher { + type Error; + fn enabled(&self) -> bool; + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + e0: G0::EdgeId, + e1: G1::EdgeId, + ) -> Result; + } + + impl EdgeMatcher for NoSemanticMatch { + type Error = Infallible; + #[inline] + fn enabled(&self) -> bool { + false + } + #[inline] + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + _e0: G0::EdgeId, + _e1: G1::EdgeId, + ) -> Result { + Ok(true) + } + } + + impl EdgeMatcher for F + where + G0: GraphBase + DataMap, + G1: GraphBase + DataMap, + F: FnMut(&G0::EdgeWeight, &G1::EdgeWeight) -> Result, + { + type Error = E; + #[inline] + fn enabled(&self) -> bool { + true + } + #[inline] + fn eq( + &mut self, + g0: &G0, + g1: &G1, + e0: G0::EdgeId, + e1: G1::EdgeId, + ) -> Result { + if let (Some(x), Some(y)) = (g0.edge_weight(e0), g1.edge_weight(e1)) { + self(x, y) + } else { + Ok(false) + } + } + } + + /// [Graph] Return `true` if the graphs `g0` and `g1` are (sub) graph isomorphic. + /// + /// Using the VF2 algorithm, examining both syntactic and semantic + /// graph isomorphism (graph structure and matching node and edge weights). + /// + /// The graphs should not be multigraphs. + pub fn is_isomorphic( + g0: &G0, + g1: &G1, + node_match: NM, + edge_match: EM, + id_order: bool, + ordering: Ordering, + induced: bool, + call_limit: Option, + ) -> Result> + where + G0: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G0: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G1: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G1::NodeWeight: Clone, + G1::EdgeWeight: Clone, + NM: NodeMatcher, + EM: EdgeMatcher, + { + if (g0.node_count().cmp(&g1.node_count()).then(ordering) != ordering) + || (g0.edge_count().cmp(&g1.edge_count()).then(ordering) != ordering) + { + return Ok(false); + } + + let mut vf2 = Vf2Algorithm::new( + g0, g1, node_match, edge_match, id_order, ordering, induced, call_limit, + ); + + match vf2.next() { + Some(Ok(_)) => Ok(true), + Some(Err(e)) => Err(e), + None => Ok(false), + } + } + + #[derive(Copy, Clone, PartialEq, Debug)] + enum OpenList { + Out, + In, + Other, + } + + #[derive(Clone, PartialEq, Debug)] + enum Frame { + Outer, + Inner { nodes: [N; 2], open_list: OpenList }, + Unwind { nodes: [N; 2], open_list: OpenList }, + } + + /// An iterator which uses the VF2(++) algorithm to produce isomorphic matches + /// between two graphs, examining both syntactic and semantic graph isomorphism + /// (graph structure and matching node and edge weights). + /// + /// The graphs should not be multigraphs. + pub struct Vf2Algorithm + where + G0: GraphBase + Data, + G1: GraphBase + Data, + NM: NodeMatcher, + EM: EdgeMatcher, + { + pub st: (Vf2State, Vf2State), + pub node_match: NM, + pub edge_match: EM, + ordering: Ordering, + induced: bool, + node_map_g0: HashMap, + node_map_g1: HashMap, + stack: Vec>, + call_limit: Option, + _counter: usize, + } + + impl Vf2Algorithm + where + G0: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G0: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G1: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G1::NodeWeight: Clone, + G1::EdgeWeight: Clone, + NM: NodeMatcher, + EM: EdgeMatcher, + { + pub fn new( + g0: &G0, + g1: &G1, + node_match: NM, + edge_match: EM, + id_order: bool, + ordering: Ordering, + induced: bool, + call_limit: Option, + ) -> Self { + let (g0, node_map_g0) = if id_order { + DefaultIdSorter::new().reorder(g0) + } else { + Vf2ppSorter::new().reorder(g0) + }; + + let (g1, node_map_g1) = if id_order { + DefaultIdSorter::new().reorder(g1) + } else { + Vf2ppSorter::new().reorder(g1) + }; + + let st = (Vf2State::new(g0), Vf2State::new(g1)); + Vf2Algorithm { + st, + node_match, + edge_match, + ordering, + induced, + node_map_g0, + node_map_g1, + stack: vec![Frame::Outer], + call_limit, + _counter: 0, + } + } + + fn mapping(&self) -> DictMap { + let mut mapping: DictMap = DictMap::new(); + self.st + .1 + .mapping + .iter() + .enumerate() + .for_each(|(index, val)| { + mapping.insert(self.node_map_g0[&val.index()], self.node_map_g1[&index]); + }); + + mapping + } + + fn next_candidate( + st: &mut (Vf2State, Vf2State), + ) -> Option<(NodeIndex, NodeIndex, OpenList)> { + // Try the out list + let mut to_index = st.1.next_out_index(0); + let mut from_index = None; + let mut open_list = OpenList::Out; + + if to_index.is_some() { + from_index = st.0.next_out_index(0); + open_list = OpenList::Out; + } + // Try the in list + if to_index.is_none() || from_index.is_none() { + to_index = st.1.next_in_index(0); + + if to_index.is_some() { + from_index = st.0.next_in_index(0); + open_list = OpenList::In; + } + } + // Try the other list -- disconnected graph + if to_index.is_none() || from_index.is_none() { + to_index = st.1.next_rest_index(0); + if to_index.is_some() { + from_index = st.0.next_rest_index(0); + open_list = OpenList::Other; + } + } + match (from_index, to_index) { + (Some(n), Some(m)) => Some((NodeIndex::new(n), NodeIndex::new(m), open_list)), + // No more candidates + _ => None, + } + } + + fn next_from_ix( + st: &mut (Vf2State, Vf2State), + nx: NodeIndex, + open_list: OpenList, + ) -> Option { + // Find the next node index to try on the `from` side of the mapping + let start = nx.index() + 1; + let cand0 = match open_list { + OpenList::Out => st.0.next_out_index(start), + OpenList::In => st.0.next_in_index(start), + OpenList::Other => st.0.next_rest_index(start), + } + .map(|c| c + start); // compensate for start offset. + match cand0 { + None => None, // no more candidates + Some(ix) => { + debug_assert!(ix >= start); + Some(NodeIndex::new(ix)) + } + } + } + + fn pop_state(st: &mut (Vf2State, Vf2State), nodes: [NodeIndex; 2]) { + // Restore state. + st.0.pop_mapping(nodes[0]); + st.1.pop_mapping(nodes[1]); + } + + fn push_state(st: &mut (Vf2State, Vf2State), nodes: [NodeIndex; 2]) { + // Add mapping nx <-> mx to the state + st.0.push_mapping(nodes[0], nodes[1]); + st.1.push_mapping(nodes[1], nodes[0]); + } + + fn is_feasible( + st: &mut (Vf2State, Vf2State), + nodes: [NodeIndex; 2], + node_match: &mut NM, + edge_match: &mut EM, + ordering: Ordering, + induced: bool, + ) -> Result> { + // Check syntactic feasibility of mapping by ensuring adjacencies + // of nx map to adjacencies of mx. + // + // nx == map to => mx + // + // R_succ + // + // Check that every neighbor of nx is mapped to a neighbor of mx, + // then check the reverse, from mx to nx. Check that they have the same + // count of edges. + // + // Note: We want to check the lookahead measures here if we can, + // R_out: Equal for G0, G1: Card(Succ(G, n) ^ Tout); for both Succ and Pred + // R_in: Same with Tin + // R_new: Equal for G0, G1: Ñ n Pred(G, n); both Succ and Pred, + // Ñ is G0 - M - Tin - Tout + let end = NodeIndex::end(); + let mut succ_count = [0, 0]; + for n_neigh in st.0.graph.neighbors(nodes[0]) { + succ_count[0] += 1; + if !induced { + continue; + } + // handle the self loop case; it's not in the mapping (yet) + let m_neigh = if nodes[0] != n_neigh { + st.0.mapping[n_neigh.index()] + } else { + nodes[1] + }; + if m_neigh == end { + continue; + } + let val = + edge_multiplicity(&st.0.graph, &st.0.adjacency_matrix, nodes[0], n_neigh); + + let has_edge = + is_adjacent(&st.1.graph, &st.1.adjacency_matrix, nodes[1], m_neigh, val); + if !has_edge { + return Ok(false); + } + } + + for n_neigh in st.1.graph.neighbors(nodes[1]) { + succ_count[1] += 1; + // handle the self loop case; it's not in the mapping (yet) + let m_neigh = if nodes[1] != n_neigh { + st.1.mapping[n_neigh.index()] + } else { + nodes[0] + }; + if m_neigh == end { + continue; + } + let val = + edge_multiplicity(&st.1.graph, &st.1.adjacency_matrix, nodes[1], n_neigh); + + let has_edge = + is_adjacent(&st.0.graph, &st.0.adjacency_matrix, nodes[0], m_neigh, val); + if !has_edge { + return Ok(false); + } + } + if succ_count[0].cmp(&succ_count[1]).then(ordering) != ordering { + return Ok(false); + } + // R_pred + if st.0.graph.is_directed() { + let mut pred_count = [0, 0]; + for n_neigh in st.0.graph.neighbors_directed(nodes[0], Incoming) { + pred_count[0] += 1; + if !induced { + continue; + } + // the self loop case is handled in outgoing + let m_neigh = st.0.mapping[n_neigh.index()]; + if m_neigh == end { + continue; + } + let val = edge_multiplicity( + &st.0.graph, + &st.0.adjacency_matrix, + n_neigh, + nodes[0], + ); + + let has_edge = is_adjacent( + &st.1.graph, + &st.1.adjacency_matrix, + m_neigh, + nodes[1], + val, + ); + if !has_edge { + return Ok(false); + } + } + + for n_neigh in st.1.graph.neighbors_directed(nodes[1], Incoming) { + pred_count[1] += 1; + // the self loop case is handled in outgoing + let m_neigh = st.1.mapping[n_neigh.index()]; + if m_neigh == end { + continue; + } + let val = edge_multiplicity( + &st.1.graph, + &st.1.adjacency_matrix, + n_neigh, + nodes[1], + ); + + let has_edge = is_adjacent( + &st.0.graph, + &st.0.adjacency_matrix, + m_neigh, + nodes[0], + val, + ); + if !has_edge { + return Ok(false); + } + } + if pred_count[0].cmp(&pred_count[1]).then(ordering) != ordering { + return Ok(false); + } + } + macro_rules! field { + ($x:ident, 0) => { + $x.0 + }; + ($x:ident, 1) => { + $x.1 + }; + ($x:ident, 1 - 0) => { + $x.1 + }; + ($x:ident, 1 - 1) => { + $x.0 + }; + } + macro_rules! rule { + ($arr:ident, $j:tt, $dir:expr) => {{ + let mut count = 0; + for n_neigh in field!(st, $j).graph.neighbors_directed(nodes[$j], $dir) { + let index = n_neigh.index(); + if field!(st, $j).$arr[index] > 0 && st.$j.mapping[index] == end { + count += 1; + } + } + count + }}; + } + // R_out + if rule!(out, 0, Outgoing) + .cmp(&rule!(out, 1, Outgoing)) + .then(ordering) + != ordering + { + return Ok(false); + } + if st.0.graph.is_directed() + && rule!(out, 0, Incoming) + .cmp(&rule!(out, 1, Incoming)) + .then(ordering) + != ordering + { + return Ok(false); + } + // R_in + if st.0.graph.is_directed() { + if rule!(ins, 0, Outgoing) + .cmp(&rule!(ins, 1, Outgoing)) + .then(ordering) + != ordering + { + return Ok(false); + } + + if rule!(ins, 0, Incoming) + .cmp(&rule!(ins, 1, Incoming)) + .then(ordering) + != ordering + { + return Ok(false); + } + } + // R_new + if induced { + let mut new_count = [0, 0]; + for n_neigh in st.0.graph.neighbors(nodes[0]) { + let index = n_neigh.index(); + if st.0.out[index] == 0 && (st.0.ins.is_empty() || st.0.ins[index] == 0) { + new_count[0] += 1; + } + } + for n_neigh in st.1.graph.neighbors(nodes[1]) { + let index = n_neigh.index(); + if st.1.out[index] == 0 && (st.1.ins.is_empty() || st.1.ins[index] == 0) { + new_count[1] += 1; + } + } + if new_count[0].cmp(&new_count[1]).then(ordering) != ordering { + return Ok(false); + } + if st.0.graph.is_directed() { + let mut new_count = [0, 0]; + for n_neigh in st.0.graph.neighbors_directed(nodes[0], Incoming) { + let index = n_neigh.index(); + if st.0.out[index] == 0 && st.0.ins[index] == 0 { + new_count[0] += 1; + } + } + for n_neigh in st.1.graph.neighbors_directed(nodes[1], Incoming) { + let index = n_neigh.index(); + if st.1.out[index] == 0 && st.1.ins[index] == 0 { + new_count[1] += 1; + } + } + if new_count[0].cmp(&new_count[1]).then(ordering) != ordering { + return Ok(false); + } + } + } + // semantic feasibility: compare associated data for nodes + if node_match.enabled() + && !node_match + .eq(&st.0.graph, &st.1.graph, nodes[0], nodes[1]) + .map_err(IsIsomorphicError::NodeMatcherErr)? + { + return Ok(false); + } + // semantic feasibility: compare associated data for edges + if edge_match.enabled() { + let mut matcher = + |g0_edge: (NodeIndex, G0::EdgeId), + g1_edge: (NodeIndex, G1::EdgeId)| + -> Result> { + let (nx, e0) = g0_edge; + let (mx, e1) = g1_edge; + if nx == mx + && edge_match + .eq(&st.0.graph, &st.1.graph, e0, e1) + .map_err(IsIsomorphicError::EdgeMatcherErr)? + { + return Ok(true); + } + Ok(false) + }; + + // Used to reverse the order of edge args to the matcher + // when checking G1 subset of G0. + #[inline] + fn reverse_args(mut f: F) -> impl FnMut(T2, T1) -> R + where + F: FnMut(T1, T2) -> R, + { + move |y, x| f(x, y) + } + + // outgoing edges + if induced { + let e_first: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges(nodes[0]) + .filter_map(|edge| { + let n_neigh = edge.target(); + let m_neigh = if nodes[0] != n_neigh { + st.0.mapping[n_neigh.index()] + } else { + nodes[1] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges(nodes[1]) + .map(|edge| (edge.target(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut matcher)? { + return Ok(false); + }; + } + + let e_first: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges(nodes[1]) + .filter_map(|edge| { + let n_neigh = edge.target(); + let m_neigh = if nodes[1] != n_neigh { + st.1.mapping[n_neigh.index()] + } else { + nodes[0] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges(nodes[0]) + .map(|edge| (edge.target(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { + return Ok(false); + }; + + // incoming edges + if st.0.graph.is_directed() { + if induced { + let e_first: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges_directed(nodes[0], Incoming) + .filter_map(|edge| { + let n_neigh = edge.source(); + let m_neigh = if nodes[0] != n_neigh { + st.0.mapping[n_neigh.index()] + } else { + nodes[1] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges_directed(nodes[1], Incoming) + .map(|edge| (edge.source(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut matcher)? { + return Ok(false); + }; + } + + let e_first: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges_directed(nodes[1], Incoming) + .filter_map(|edge| { + let n_neigh = edge.source(); + let m_neigh = if nodes[1] != n_neigh { + st.1.mapping[n_neigh.index()] + } else { + nodes[0] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges_directed(nodes[0], Incoming) + .map(|edge| (edge.source(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { + return Ok(false); + }; + } + } + Ok(true) + } + } + + impl Iterator for Vf2Algorithm + where + G0: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G0: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G1: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G1::NodeWeight: Clone, + G1::EdgeWeight: Clone, + NM: NodeMatcher, + EM: EdgeMatcher, + { + type Item = Result, IsIsomorphicError>; + + /// Return Some(mapping) if isomorphism is decided, else None. + fn next(&mut self) -> Option { + if (self + .st + .0 + .graph + .node_count() + .cmp(&self.st.1.graph.node_count()) + .then(self.ordering) + != self.ordering) + || (self + .st + .0 + .graph + .edge_count() + .cmp(&self.st.1.graph.edge_count()) + .then(self.ordering) + != self.ordering) + { + return None; + } + + // A "depth first" search of a valid mapping from graph 1 to graph 2 + + // F(s, n, m) -- evaluate state s and add mapping n <-> m + + // Find least T1out node (in st.out[1] but not in M[1]) + while let Some(frame) = self.stack.pop() { + match frame { + Frame::Unwind { + nodes, + open_list: ol, + } => { + Vf2Algorithm::::pop_state(&mut self.st, nodes); + + match Vf2Algorithm::::next_from_ix( + &mut self.st, + nodes[0], + ol, + ) { + None => continue, + Some(nx) => { + let f = Frame::Inner { + nodes: [nx, nodes[1]], + open_list: ol, + }; + self.stack.push(f); + } + } + } + Frame::Outer => { + match Vf2Algorithm::::next_candidate(&mut self.st) { + None => { + if self.st.1.is_complete() { + return Some(Ok(self.mapping())); + } + continue; + } + Some((nx, mx, ol)) => { + let f = Frame::Inner { + nodes: [nx, mx], + open_list: ol, + }; + self.stack.push(f); + } + } + } + Frame::Inner { + nodes, + open_list: ol, + } => { + let feasible = match Vf2Algorithm::::is_feasible( + &mut self.st, + nodes, + &mut self.node_match, + &mut self.edge_match, + self.ordering, + self.induced, + ) { + Ok(f) => f, + Err(e) => { + return Some(Err(e)); + } + }; + + if feasible { + Vf2Algorithm::::push_state(&mut self.st, nodes); + // Check cardinalities of Tin, Tout sets + if self + .st + .0 + .out_size + .cmp(&self.st.1.out_size) + .then(self.ordering) + == self.ordering + && self + .st + .0 + .ins_size + .cmp(&self.st.1.ins_size) + .then(self.ordering) + == self.ordering + { + self._counter += 1; + if let Some(limit) = self.call_limit { + if self._counter > limit { + return None; + } + } + let f0 = Frame::Unwind { + nodes, + open_list: ol, + }; + + self.stack.push(f0); + self.stack.push(Frame::Outer); + continue; + } + Vf2Algorithm::::pop_state(&mut self.st, nodes); + } + match Vf2Algorithm::::next_from_ix( + &mut self.st, + nodes[0], + ol, + ) { + None => continue, + Some(nx) => { + let f = Frame::Inner { + nodes: [nx, nodes[1]], + open_list: ol, + }; + self.stack.push(f); + } + } + } + } + } + None + } + } + } +} diff --git a/qiskit/converters/circuit_to_dag.py b/qiskit/converters/circuit_to_dag.py index 10a48df99778..a330b8cbd682 100644 --- a/qiskit/converters/circuit_to_dag.py +++ b/qiskit/converters/circuit_to_dag.py @@ -12,7 +12,8 @@ """Helper function for converting a circuit to a dag""" -from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagnode import DAGOpNode def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_order=None): @@ -93,7 +94,7 @@ def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_ord for instruction in circuit.data: dagcircuit._apply_op_node_back( - DAGOpNode.from_instruction(instruction, dag=dagcircuit, deepcopy=copy_operations) + DAGOpNode.from_instruction(instruction, deepcopy=copy_operations) ) dagcircuit.duration = circuit.duration diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 53cbc6f8f7f1..8738c2676a14 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -20,2406 +20,5 @@ composed, and modified. Some natural properties like depth can be computed directly from the graph. """ -from __future__ import annotations -import copy -import enum -import itertools -import math -from collections import OrderedDict, defaultdict, deque, namedtuple -from collections.abc import Callable, Sequence, Generator, Iterable -from typing import Any, Literal - -import numpy as np -import rustworkx as rx - -from qiskit.circuit import ( - ControlFlowOp, - ForLoopOp, - IfElseOp, - WhileLoopOp, - SwitchCaseOp, - _classical_resource_map, - Operation, - Store, -) -from qiskit.circuit.classical import expr -from qiskit.circuit.controlflow import condition_resources, node_resources, CONTROL_FLOW_OP_NAMES -from qiskit.circuit.quantumregister import QuantumRegister, Qubit -from qiskit.circuit.classicalregister import ClassicalRegister, Clbit -from qiskit.circuit.gate import Gate -from qiskit.circuit.instruction import Instruction -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.dagcircuit.exceptions import DAGCircuitError -from qiskit.dagcircuit.dagnode import DAGNode, DAGOpNode, DAGInNode, DAGOutNode -from qiskit.circuit.bit import Bit -from qiskit.pulse import Schedule -from qiskit._accelerate.euler_one_qubit_decomposer import collect_1q_runs_filter -from qiskit._accelerate.convert_2q_block_matrix import collect_2q_blocks_filter - -BitLocations = namedtuple("BitLocations", ("index", "registers")) -# The allowable arguments to :meth:`DAGCircuit.copy_empty_like`'s ``vars_mode``. -_VarsMode = Literal["alike", "captures", "drop"] - - -class DAGCircuit: - """ - Quantum circuit as a directed acyclic graph. - - There are 3 types of nodes in the graph: inputs, outputs, and operations. - The nodes are connected by directed edges that correspond to qubits and - bits. - """ - - # pylint: disable=invalid-name - - def __init__(self): - """Create an empty circuit.""" - - # Circuit name. Generally, this corresponds to the name - # of the QuantumCircuit from which the DAG was generated. - self.name = None - - # Circuit metadata - self.metadata = {} - - # Cache of dag op node sort keys - self._key_cache = {} - - # Set of wire data in the DAG. A wire is an owned unit of data. Qubits are the primary - # wire type (and the only data that has _true_ wire properties from a read/write - # perspective), but clbits and classical `Var`s are too. Note: classical registers are - # _not_ wires because the individual bits are the more fundamental unit. We treat `Var`s - # as the entire wire (as opposed to individual bits of them) for scalability reasons; if a - # parametric program wants to parametrize over 16-bit angles, we can't scale to 1000s of - # those by tracking all 16 bits individually. - # - # Classical variables shouldn't be "wires"; it should be possible to have multiple reads - # without implying ordering. The initial addition of the classical variables uses the - # existing wire structure as an MVP; we expect to handle this better in a new version of the - # transpiler IR that also handles control flow more properly. - self._wires = set() - - # Map from wire to input nodes of the graph - self.input_map = OrderedDict() - - # Map from wire to output nodes of the graph - self.output_map = OrderedDict() - - # Directed multigraph whose nodes are inputs, outputs, or operations. - # Operation nodes have equal in- and out-degrees and carry - # additional data about the operation, including the argument order - # and parameter values. - # Input nodes have out-degree 1 and output nodes have in-degree 1. - # Edges carry wire labels and each operation has - # corresponding in- and out-edges with the same wire labels. - self._multi_graph = rx.PyDAG() - - # Map of qreg/creg name to Register object. - self.qregs = OrderedDict() - self.cregs = OrderedDict() - - # List of Qubit/Clbit wires that the DAG acts on. - self.qubits: list[Qubit] = [] - self.clbits: list[Clbit] = [] - - # Dictionary mapping of Qubit and Clbit instances to a tuple comprised of - # 0) corresponding index in dag.{qubits,clbits} and - # 1) a list of Register-int pairs for each Register containing the Bit and - # its index within that register. - self._qubit_indices: dict[Qubit, BitLocations] = {} - self._clbit_indices: dict[Clbit, BitLocations] = {} - # Tracking for the classical variables used in the circuit. This contains the information - # needed to insert new nodes. This is keyed by the name rather than the `Var` instance - # itself so we can ensure we don't allow shadowing or redefinition of names. - self._vars_info: dict[str, _DAGVarInfo] = {} - # Convenience stateful tracking for the individual types of nodes to allow things like - # comparisons between circuits to take place without needing to disambiguate the - # graph-specific usage information. - self._vars_by_type: dict[_DAGVarType, set[expr.Var]] = { - type_: set() for type_ in _DAGVarType - } - - self._global_phase: float | ParameterExpression = 0.0 - self._calibrations: dict[str, dict[tuple, Schedule]] = defaultdict(dict) - - self._op_names = {} - - self.duration = None - self.unit = "dt" - - @property - def wires(self): - """Return a list of the wires in order.""" - return ( - self.qubits - + self.clbits - + [var for vars in self._vars_by_type.values() for var in vars] - ) - - @property - def node_counter(self): - """ - Returns the number of nodes in the dag. - """ - return len(self._multi_graph) - - @property - def global_phase(self): - """Return the global phase of the circuit.""" - return self._global_phase - - @global_phase.setter - def global_phase(self, angle: float | ParameterExpression): - """Set the global phase of the circuit. - - Args: - angle (float, ParameterExpression) - """ - if isinstance(angle, ParameterExpression): - self._global_phase = angle - else: - # Set the phase to the [0, 2Ï€) interval - angle = float(angle) - if not angle: - self._global_phase = 0 - else: - self._global_phase = angle % (2 * math.pi) - - @property - def calibrations(self) -> dict[str, dict[tuple, Schedule]]: - """Return calibration dictionary. - - The custom pulse definition of a given gate is of the form - {'gate_name': {(qubits, params): schedule}} - """ - return dict(self._calibrations) - - @calibrations.setter - def calibrations(self, calibrations: dict[str, dict[tuple, Schedule]]): - """Set the circuit calibration data from a dictionary of calibration definition. - - Args: - calibrations (dict): A dictionary of input in the format - {'gate_name': {(qubits, gate_params): schedule}} - """ - self._calibrations = defaultdict(dict, calibrations) - - def add_calibration(self, gate, qubits, schedule, params=None): - """Register a low-level, custom pulse definition for the given gate. - - Args: - gate (Union[Gate, str]): Gate information. - qubits (Union[int, Tuple[int]]): List of qubits to be measured. - schedule (Schedule): Schedule information. - params (Optional[List[Union[float, Parameter]]]): A list of parameters. - - Raises: - Exception: if the gate is of type string and params is None. - """ - - def _format(operand): - try: - # Using float/complex value as a dict key is not good idea. - # This makes the mapping quite sensitive to the rounding error. - # However, the mechanism is already tied to the execution model (i.e. pulse gate) - # and we cannot easily update this rule. - # The same logic exists in QuantumCircuit.add_calibration. - evaluated = complex(operand) - if np.isreal(evaluated): - evaluated = float(evaluated.real) - if evaluated.is_integer(): - evaluated = int(evaluated) - return evaluated - except TypeError: - # Unassigned parameter - return operand - - if isinstance(gate, Gate): - params = gate.params - gate = gate.name - if params is not None: - params = tuple(map(_format, params)) - else: - params = () - - self._calibrations[gate][(tuple(qubits), params)] = schedule - - def has_calibration_for(self, node): - """Return True if the dag has a calibration defined for the node operation. In this - case, the operation does not need to be translated to the device basis. - """ - if not self.calibrations or node.op.name not in self.calibrations: - return False - qubits = tuple(self.qubits.index(qubit) for qubit in node.qargs) - params = [] - for p in node.op.params: - if isinstance(p, ParameterExpression) and not p.parameters: - params.append(float(p)) - else: - params.append(p) - params = tuple(params) - return (qubits, params) in self.calibrations[node.op.name] - - def remove_all_ops_named(self, opname): - """Remove all operation nodes with the given name.""" - for n in self.named_nodes(opname): - self.remove_op_node(n) - - def add_qubits(self, qubits): - """Add individual qubit wires.""" - if any(not isinstance(qubit, Qubit) for qubit in qubits): - raise DAGCircuitError("not a Qubit instance.") - - duplicate_qubits = set(self.qubits).intersection(qubits) - if duplicate_qubits: - raise DAGCircuitError(f"duplicate qubits {duplicate_qubits}") - - for qubit in qubits: - self.qubits.append(qubit) - self._qubit_indices[qubit] = BitLocations(len(self.qubits) - 1, []) - self._add_wire(qubit) - - def add_clbits(self, clbits): - """Add individual clbit wires.""" - if any(not isinstance(clbit, Clbit) for clbit in clbits): - raise DAGCircuitError("not a Clbit instance.") - - duplicate_clbits = set(self.clbits).intersection(clbits) - if duplicate_clbits: - raise DAGCircuitError(f"duplicate clbits {duplicate_clbits}") - - for clbit in clbits: - self.clbits.append(clbit) - self._clbit_indices[clbit] = BitLocations(len(self.clbits) - 1, []) - self._add_wire(clbit) - - def add_qreg(self, qreg): - """Add all wires in a quantum register.""" - if not isinstance(qreg, QuantumRegister): - raise DAGCircuitError("not a QuantumRegister instance.") - if qreg.name in self.qregs: - raise DAGCircuitError(f"duplicate register {qreg.name}") - self.qregs[qreg.name] = qreg - existing_qubits = set(self.qubits) - for j in range(qreg.size): - if qreg[j] in self._qubit_indices: - self._qubit_indices[qreg[j]].registers.append((qreg, j)) - if qreg[j] not in existing_qubits: - self.qubits.append(qreg[j]) - self._qubit_indices[qreg[j]] = BitLocations( - len(self.qubits) - 1, registers=[(qreg, j)] - ) - self._add_wire(qreg[j]) - - def add_creg(self, creg): - """Add all wires in a classical register.""" - if not isinstance(creg, ClassicalRegister): - raise DAGCircuitError("not a ClassicalRegister instance.") - if creg.name in self.cregs: - raise DAGCircuitError(f"duplicate register {creg.name}") - self.cregs[creg.name] = creg - existing_clbits = set(self.clbits) - for j in range(creg.size): - if creg[j] in self._clbit_indices: - self._clbit_indices[creg[j]].registers.append((creg, j)) - if creg[j] not in existing_clbits: - self.clbits.append(creg[j]) - self._clbit_indices[creg[j]] = BitLocations( - len(self.clbits) - 1, registers=[(creg, j)] - ) - self._add_wire(creg[j]) - - def add_input_var(self, var: expr.Var): - """Add an input variable to the circuit. - - Args: - var: the variable to add.""" - if self._vars_by_type[_DAGVarType.CAPTURE]: - raise DAGCircuitError("cannot add inputs to a circuit with captures") - self._add_var(var, _DAGVarType.INPUT) - - def add_captured_var(self, var: expr.Var): - """Add a captured variable to the circuit. - - Args: - var: the variable to add.""" - if self._vars_by_type[_DAGVarType.INPUT]: - raise DAGCircuitError("cannot add captures to a circuit with inputs") - self._add_var(var, _DAGVarType.CAPTURE) - - def add_declared_var(self, var: expr.Var): - """Add a declared local variable to the circuit. - - Args: - var: the variable to add.""" - self._add_var(var, _DAGVarType.DECLARE) - - def _add_var(self, var: expr.Var, type_: _DAGVarType): - """Inner function to add any variable to the DAG. ``location`` should be a reference one of - the ``self._vars_*`` tracking dictionaries. - """ - # The setup of the initial graph structure between an "in" and an "out" node is the same as - # the bit-related `_add_wire`, but this logically needs to do different bookkeeping around - # tracking the properties. - if not var.standalone: - raise DAGCircuitError( - "cannot add variables that wrap `Clbit` or `ClassicalRegister` instances" - ) - if (previous := self._vars_info.get(var.name, None)) is not None: - if previous.var == var: - raise DAGCircuitError(f"'{var}' is already present in the circuit") - raise DAGCircuitError( - f"cannot add '{var}' as its name shadows the existing '{previous.var}'" - ) - in_node = DAGInNode(wire=var) - out_node = DAGOutNode(wire=var) - in_node._node_id, out_node._node_id = self._multi_graph.add_nodes_from((in_node, out_node)) - self._multi_graph.add_edge(in_node._node_id, out_node._node_id, var) - self.input_map[var] = in_node - self.output_map[var] = out_node - self._vars_by_type[type_].add(var) - self._vars_info[var.name] = _DAGVarInfo(var, type_, in_node, out_node) - - def _add_wire(self, wire): - """Add a qubit or bit to the circuit. - - Args: - wire (Bit): the wire to be added - - This adds a pair of in and out nodes connected by an edge. - - Raises: - DAGCircuitError: if trying to add duplicate wire - """ - if wire not in self._wires: - self._wires.add(wire) - - inp_node = DAGInNode(wire=wire) - outp_node = DAGOutNode(wire=wire) - input_map_id, output_map_id = self._multi_graph.add_nodes_from([inp_node, outp_node]) - inp_node._node_id = input_map_id - outp_node._node_id = output_map_id - self.input_map[wire] = inp_node - self.output_map[wire] = outp_node - self._multi_graph.add_edge(inp_node._node_id, outp_node._node_id, wire) - else: - raise DAGCircuitError(f"duplicate wire {wire}") - - def find_bit(self, bit: Bit) -> BitLocations: - """ - Finds locations in the circuit, by mapping the Qubit and Clbit to positional index - BitLocations is defined as: BitLocations = namedtuple("BitLocations", ("index", "registers")) - - Args: - bit (Bit): The bit to locate. - - Returns: - namedtuple(int, List[Tuple(Register, int)]): A 2-tuple. The first element (``index``) - contains the index at which the ``Bit`` can be found (in either - :obj:`~DAGCircuit.qubits`, :obj:`~DAGCircuit.clbits`, depending on its - type). The second element (``registers``) is a list of ``(register, index)`` - pairs with an entry for each :obj:`~Register` in the circuit which contains the - :obj:`~Bit` (and the index in the :obj:`~Register` at which it can be found). - - Raises: - DAGCircuitError: If the supplied :obj:`~Bit` was of an unknown type. - DAGCircuitError: If the supplied :obj:`~Bit` could not be found on the circuit. - """ - try: - if isinstance(bit, Qubit): - return self._qubit_indices[bit] - elif isinstance(bit, Clbit): - return self._clbit_indices[bit] - else: - raise DAGCircuitError(f"Could not locate bit of unknown type: {type(bit)}") - except KeyError as err: - raise DAGCircuitError( - f"Could not locate provided bit: {bit}. Has it been added to the DAGCircuit?" - ) from err - - def remove_clbits(self, *clbits): - """ - Remove classical bits from the circuit. All bits MUST be idle. - Any registers with references to at least one of the specified bits will - also be removed. - - Args: - clbits (List[Clbit]): The bits to remove. - - Raises: - DAGCircuitError: a clbit is not a :obj:`.Clbit`, is not in the circuit, - or is not idle. - """ - if any(not isinstance(clbit, Clbit) for clbit in clbits): - raise DAGCircuitError( - f"clbits not of type Clbit: {[b for b in clbits if not isinstance(b, Clbit)]}" - ) - - clbits = set(clbits) - unknown_clbits = clbits.difference(self.clbits) - if unknown_clbits: - raise DAGCircuitError(f"clbits not in circuit: {unknown_clbits}") - - busy_clbits = {bit for bit in clbits if not self._is_wire_idle(bit)} - if busy_clbits: - raise DAGCircuitError(f"clbits not idle: {busy_clbits}") - - # remove any references to bits - cregs_to_remove = {creg for creg in self.cregs.values() if not clbits.isdisjoint(creg)} - self.remove_cregs(*cregs_to_remove) - - for clbit in clbits: - self._remove_idle_wire(clbit) - self.clbits.remove(clbit) - del self._clbit_indices[clbit] - - # Update the indices of remaining clbits - for i, clbit in enumerate(self.clbits): - self._clbit_indices[clbit] = self._clbit_indices[clbit]._replace(index=i) - - def remove_cregs(self, *cregs): - """ - Remove classical registers from the circuit, leaving underlying bits - in place. - - Raises: - DAGCircuitError: a creg is not a ClassicalRegister, or is not in - the circuit. - """ - if any(not isinstance(creg, ClassicalRegister) for creg in cregs): - raise DAGCircuitError( - "cregs not of type ClassicalRegister: " - f"{[r for r in cregs if not isinstance(r, ClassicalRegister)]}" - ) - - unknown_cregs = set(cregs).difference(self.cregs.values()) - if unknown_cregs: - raise DAGCircuitError(f"cregs not in circuit: {unknown_cregs}") - - for creg in cregs: - del self.cregs[creg.name] - for j in range(creg.size): - bit = creg[j] - bit_position = self._clbit_indices[bit] - bit_position.registers.remove((creg, j)) - - def remove_qubits(self, *qubits): - """ - Remove quantum bits from the circuit. All bits MUST be idle. - Any registers with references to at least one of the specified bits will - also be removed. - - Args: - qubits (List[~qiskit.circuit.Qubit]): The bits to remove. - - Raises: - DAGCircuitError: a qubit is not a :obj:`~.circuit.Qubit`, is not in the circuit, - or is not idle. - """ - if any(not isinstance(qubit, Qubit) for qubit in qubits): - raise DAGCircuitError( - f"qubits not of type Qubit: {[b for b in qubits if not isinstance(b, Qubit)]}" - ) - - qubits = set(qubits) - unknown_qubits = qubits.difference(self.qubits) - if unknown_qubits: - raise DAGCircuitError(f"qubits not in circuit: {unknown_qubits}") - - busy_qubits = {bit for bit in qubits if not self._is_wire_idle(bit)} - if busy_qubits: - raise DAGCircuitError(f"qubits not idle: {busy_qubits}") - - # remove any references to bits - qregs_to_remove = {qreg for qreg in self.qregs.values() if not qubits.isdisjoint(qreg)} - self.remove_qregs(*qregs_to_remove) - - for qubit in qubits: - self._remove_idle_wire(qubit) - self.qubits.remove(qubit) - del self._qubit_indices[qubit] - - # Update the indices of remaining qubits - for i, qubit in enumerate(self.qubits): - self._qubit_indices[qubit] = self._qubit_indices[qubit]._replace(index=i) - - def remove_qregs(self, *qregs): - """ - Remove quantum registers from the circuit, leaving underlying bits - in place. - - Raises: - DAGCircuitError: a qreg is not a QuantumRegister, or is not in - the circuit. - """ - if any(not isinstance(qreg, QuantumRegister) for qreg in qregs): - raise DAGCircuitError( - f"qregs not of type QuantumRegister: " - f"{[r for r in qregs if not isinstance(r, QuantumRegister)]}" - ) - - unknown_qregs = set(qregs).difference(self.qregs.values()) - if unknown_qregs: - raise DAGCircuitError(f"qregs not in circuit: {unknown_qregs}") - - for qreg in qregs: - del self.qregs[qreg.name] - for j in range(qreg.size): - bit = qreg[j] - bit_position = self._qubit_indices[bit] - bit_position.registers.remove((qreg, j)) - - def _is_wire_idle(self, wire): - """Check if a wire is idle. - - Args: - wire (Bit): a wire in the circuit. - - Returns: - bool: true if the wire is idle, false otherwise. - - Raises: - DAGCircuitError: the wire is not in the circuit. - """ - if wire not in self._wires: - raise DAGCircuitError(f"wire {wire} not in circuit") - - try: - child = next(self.successors(self.input_map[wire])) - except StopIteration as e: - raise DAGCircuitError( - f"Invalid dagcircuit input node {self.input_map[wire]} has no output" - ) from e - return child is self.output_map[wire] - - def _remove_idle_wire(self, wire): - """Remove an idle qubit or bit from the circuit. - - Args: - wire (Bit): the wire to be removed, which MUST be idle. - """ - inp_node = self.input_map[wire] - oup_node = self.output_map[wire] - - self._multi_graph.remove_node(inp_node._node_id) - self._multi_graph.remove_node(oup_node._node_id) - self._wires.remove(wire) - del self.input_map[wire] - del self.output_map[wire] - - def _check_condition(self, name, condition): - """Verify that the condition is valid. - - Args: - name (string): used for error reporting - condition (tuple or None): a condition tuple (ClassicalRegister, int) or (Clbit, bool) - - Raises: - DAGCircuitError: if conditioning on an invalid register - """ - if condition is None: - return - resources = condition_resources(condition) - for creg in resources.cregs: - if creg.name not in self.cregs: - raise DAGCircuitError(f"invalid creg in condition for {name}") - if not set(resources.clbits).issubset(self.clbits): - raise DAGCircuitError(f"invalid clbits in condition for {name}") - - def _check_wires(self, args: Iterable[Bit | expr.Var], amap: dict[Bit | expr.Var, Any]): - """Check the values of a list of wire arguments. - - For each element of args, check that amap contains it. - - Args: - args: the elements to be checked - amap: a dictionary keyed on Qubits/Clbits - - Raises: - DAGCircuitError: if a qubit is not contained in amap - """ - # Check for each wire - for wire in args: - if wire not in amap: - raise DAGCircuitError(f"wire {wire} not found in {amap}") - - def _increment_op(self, op_name): - if op_name in self._op_names: - self._op_names[op_name] += 1 - else: - self._op_names[op_name] = 1 - - def _decrement_op(self, op_name): - if self._op_names[op_name] == 1: - del self._op_names[op_name] - else: - self._op_names[op_name] -= 1 - - def copy_empty_like(self, *, vars_mode: _VarsMode = "alike"): - """Return a copy of self with the same structure but empty. - - That structure includes: - * name and other metadata - * global phase - * duration - * all the qubits and clbits, including the registers - * all the classical variables, with a mode defined by ``vars_mode``. - - Args: - vars_mode: The mode to handle realtime variables in. - - alike - The variables in the output DAG will have the same declaration semantics as - in the original circuit. For example, ``input`` variables in the source will be - ``input`` variables in the output DAG. - - captures - All variables will be converted to captured variables. This is useful when you - are building a new layer for an existing DAG that you will want to - :meth:`compose` onto the base, since :meth:`compose` can inline captures onto - the base circuit (but not other variables). - - drop - The output DAG will have no variables defined. - - Returns: - DAGCircuit: An empty copy of self. - """ - target_dag = DAGCircuit() - target_dag.name = self.name - target_dag._global_phase = self._global_phase - target_dag.duration = self.duration - target_dag.unit = self.unit - target_dag.metadata = self.metadata - target_dag._key_cache = self._key_cache - - target_dag.add_qubits(self.qubits) - target_dag.add_clbits(self.clbits) - - for qreg in self.qregs.values(): - target_dag.add_qreg(qreg) - for creg in self.cregs.values(): - target_dag.add_creg(creg) - - if vars_mode == "alike": - for var in self.iter_input_vars(): - target_dag.add_input_var(var) - for var in self.iter_captured_vars(): - target_dag.add_captured_var(var) - for var in self.iter_declared_vars(): - target_dag.add_declared_var(var) - elif vars_mode == "captures": - for var in self.iter_vars(): - target_dag.add_captured_var(var) - elif vars_mode == "drop": - pass - else: # pragma: no cover - raise ValueError(f"unknown vars_mode: '{vars_mode}'") - - return target_dag - - def _apply_op_node_back(self, node: DAGOpNode, *, check: bool = False): - additional = () - if _may_have_additional_wires(node): - # This is the slow path; most of the time, this won't happen. - additional = set(_additional_wires(node.op)).difference(node.cargs) - - if check: - self._check_condition(node.name, node.condition) - self._check_wires(node.qargs, self.output_map) - self._check_wires(node.cargs, self.output_map) - self._check_wires(additional, self.output_map) - - node._node_id = self._multi_graph.add_node(node) - self._increment_op(node.name) - - # Add new in-edges from predecessors of the output nodes to the - # operation node while deleting the old in-edges of the output nodes - # and adding new edges from the operation node to each output node - self._multi_graph.insert_node_on_in_edges_multiple( - node._node_id, - [ - self.output_map[bit]._node_id - for bits in (node.qargs, node.cargs, additional) - for bit in bits - ], - ) - return node - - def apply_operation_back( - self, - op: Operation, - qargs: Iterable[Qubit] = (), - cargs: Iterable[Clbit] = (), - *, - check: bool = True, - ) -> DAGOpNode: - """Apply an operation to the output of the circuit. - - Args: - op (qiskit.circuit.Operation): the operation associated with the DAG node - qargs (tuple[~qiskit.circuit.Qubit]): qubits that op will be applied to - cargs (tuple[Clbit]): cbits that op will be applied to - check (bool): If ``True`` (default), this function will enforce that the - :class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are - :class:`~.circuit.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must* - uphold these invariants itself, but the cost of several checks will be skipped. - This is most useful when building a new DAG from a source of known-good nodes. - Returns: - DAGOpNode: the node for the op that was added to the dag - - Raises: - DAGCircuitError: if a leaf node is connected to multiple outputs - - """ - return self._apply_op_node_back( - DAGOpNode(op=op, qargs=tuple(qargs), cargs=tuple(cargs), dag=self), check=check - ) - - def apply_operation_front( - self, - op: Operation, - qargs: Sequence[Qubit] = (), - cargs: Sequence[Clbit] = (), - *, - check: bool = True, - ) -> DAGOpNode: - """Apply an operation to the input of the circuit. - - Args: - op (qiskit.circuit.Operation): the operation associated with the DAG node - qargs (tuple[~qiskit.circuit.Qubit]): qubits that op will be applied to - cargs (tuple[Clbit]): cbits that op will be applied to - check (bool): If ``True`` (default), this function will enforce that the - :class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are - :class:`~.circuit.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must* - uphold these invariants itself, but the cost of several checks will be skipped. - This is most useful when building a new DAG from a source of known-good nodes. - Returns: - DAGOpNode: the node for the op that was added to the dag - - Raises: - DAGCircuitError: if initial nodes connected to multiple out edges - """ - qargs = tuple(qargs) - cargs = tuple(cargs) - additional = () - - node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) - if _may_have_additional_wires(node): - # This is the slow path; most of the time, this won't happen. - additional = set(_additional_wires(node.op)).difference(cargs) - - if check: - self._check_condition(node.name, node.condition) - self._check_wires(node.qargs, self.output_map) - self._check_wires(node.cargs, self.output_map) - self._check_wires(additional, self.output_map) - - node._node_id = self._multi_graph.add_node(node) - self._increment_op(node.name) - - # Add new out-edges to successors of the input nodes from the - # operation node while deleting the old out-edges of the input nodes - # and adding new edges to the operation node from each input node - self._multi_graph.insert_node_on_out_edges_multiple( - node._node_id, - [ - self.input_map[bit]._node_id - for bits in (node.qargs, node.cargs, additional) - for bit in bits - ], - ) - return node - - def compose( - self, other, qubits=None, clbits=None, front=False, inplace=True, *, inline_captures=False - ): - """Compose the ``other`` circuit onto the output of this circuit. - - A subset of input wires of ``other`` are mapped - to a subset of output wires of this circuit. - - ``other`` can be narrower or of equal width to ``self``. - - Args: - other (DAGCircuit): circuit to compose with self - qubits (list[~qiskit.circuit.Qubit|int]): qubits of self to compose onto. - clbits (list[Clbit|int]): clbits of self to compose onto. - front (bool): If True, front composition will be performed (not implemented yet) - inplace (bool): If True, modify the object. Otherwise return composed circuit. - inline_captures (bool): If ``True``, variables marked as "captures" in the ``other`` DAG - will inlined onto existing uses of those same variables in ``self``. If ``False``, - all variables in ``other`` are required to be distinct from ``self``, and they will - be added to ``self``. - - .. - Note: unlike `QuantumCircuit.compose`, there's no `var_remap` argument here. That's - because the `DAGCircuit` inner-block structure isn't set up well to allow the recursion, - and `DAGCircuit.compose` is generally only used to rebuild a DAG from layers within - itself than to join unrelated circuits. While there's no strong motivating use-case - (unlike the `QuantumCircuit` equivalent), it's safer and more performant to not provide - the option. - - Returns: - DAGCircuit: the composed dag (returns None if inplace==True). - - Raises: - DAGCircuitError: if ``other`` is wider or there are duplicate edge mappings. - """ - if front: - raise DAGCircuitError("Front composition not supported yet.") - - if len(other.qubits) > len(self.qubits) or len(other.clbits) > len(self.clbits): - raise DAGCircuitError( - "Trying to compose with another DAGCircuit which has more 'in' edges." - ) - - # number of qubits and clbits must match number in circuit or None - identity_qubit_map = dict(zip(other.qubits, self.qubits)) - identity_clbit_map = dict(zip(other.clbits, self.clbits)) - if qubits is None: - qubit_map = identity_qubit_map - elif len(qubits) != len(other.qubits): - raise DAGCircuitError( - "Number of items in qubits parameter does not" - " match number of qubits in the circuit." - ) - else: - qubit_map = { - other.qubits[i]: (self.qubits[q] if isinstance(q, int) else q) - for i, q in enumerate(qubits) - } - if clbits is None: - clbit_map = identity_clbit_map - elif len(clbits) != len(other.clbits): - raise DAGCircuitError( - "Number of items in clbits parameter does not" - " match number of clbits in the circuit." - ) - else: - clbit_map = { - other.clbits[i]: (self.clbits[c] if isinstance(c, int) else c) - for i, c in enumerate(clbits) - } - edge_map = {**qubit_map, **clbit_map} or None - - # if no edge_map, try to do a 1-1 mapping in order - if edge_map is None: - edge_map = {**identity_qubit_map, **identity_clbit_map} - - # Check the edge_map for duplicate values - if len(set(edge_map.values())) != len(edge_map): - raise DAGCircuitError("duplicates in wire_map") - - # Compose - if inplace: - dag = self - else: - dag = copy.deepcopy(self) - dag.global_phase += other.global_phase - - for gate, cals in other.calibrations.items(): - dag._calibrations[gate].update(cals) - - # This is all the handling we need for realtime variables, if there's no remapping. They: - # - # * get added to the DAG and then operations involving them get appended on normally. - # * get inlined onto an existing variable, then operations get appended normally. - # * there's a clash or a failed inlining, and we just raise an error. - # - # Notably if there's no remapping, there's no need to recurse into control-flow or to do any - # Var rewriting during the Expr visits. - for var in other.iter_input_vars(): - dag.add_input_var(var) - if inline_captures: - for var in other.iter_captured_vars(): - if not dag.has_var(var): - raise DAGCircuitError( - f"Variable '{var}' to be inlined is not in the base DAG." - " If you wanted it to be automatically added, use `inline_captures=False`." - ) - else: - for var in other.iter_captured_vars(): - dag.add_captured_var(var) - for var in other.iter_declared_vars(): - dag.add_declared_var(var) - - # Ensure that the error raised here is a `DAGCircuitError` for backwards compatibility. - def _reject_new_register(reg): - raise DAGCircuitError(f"No register with '{reg.bits}' to map this expression onto.") - - variable_mapper = _classical_resource_map.VariableMapper( - dag.cregs.values(), edge_map, add_register=_reject_new_register - ) - for nd in other.topological_nodes(): - if isinstance(nd, DAGInNode): - if isinstance(nd.wire, Bit): - # if in edge_map, get new name, else use existing name - m_wire = edge_map.get(nd.wire, nd.wire) - # the mapped wire should already exist - if m_wire not in dag.output_map: - raise DAGCircuitError( - f"wire {m_wire.register.name}[{m_wire.index}] not in self" - ) - if nd.wire not in other._wires: - raise DAGCircuitError( - f"inconsistent wire type for {nd.register.name}[{nd.wire.index}] in other" - ) - # If it's a Var wire, we already checked that it exists in the destination. - elif isinstance(nd, DAGOutNode): - # ignore output nodes - pass - elif isinstance(nd, DAGOpNode): - m_qargs = [edge_map.get(x, x) for x in nd.qargs] - m_cargs = [edge_map.get(x, x) for x in nd.cargs] - inst = nd._to_circuit_instruction(deepcopy=True) - m_op = None - if inst.condition is not None: - if inst.is_control_flow(): - m_op = inst.operation - m_op.condition = variable_mapper.map_condition( - inst.condition, allow_reorder=True - ) - else: - m_op = inst.operation.c_if( - *variable_mapper.map_condition(inst.condition, allow_reorder=True) - ) - elif inst.is_control_flow() and isinstance(inst.operation, SwitchCaseOp): - m_op = inst.operation - m_op.target = variable_mapper.map_target(m_op.target) - if m_op is None: - inst = inst.replace(qubits=m_qargs, clbits=m_cargs) - else: - inst = inst.replace(operation=m_op, qubits=m_qargs, clbits=m_cargs) - dag._apply_op_node_back(DAGOpNode.from_instruction(inst), check=False) - else: - raise DAGCircuitError(f"bad node type {type(nd)}") - - if not inplace: - return dag - else: - return None - - def reverse_ops(self): - """Reverse the operations in the ``self`` circuit. - - Returns: - DAGCircuit: the reversed dag. - """ - # TODO: speed up - # pylint: disable=cyclic-import - from qiskit.converters import dag_to_circuit, circuit_to_dag - - qc = dag_to_circuit(self) - reversed_qc = qc.reverse_ops() - reversed_dag = circuit_to_dag(reversed_qc) - return reversed_dag - - def idle_wires(self, ignore=None): - """Return idle wires. - - Args: - ignore (list(str)): List of node names to ignore. Default: [] - - Yields: - Bit: Bit in idle wire. - - Raises: - DAGCircuitError: If the DAG is invalid - """ - if ignore is None: - ignore = set() - ignore_set = set(ignore) - for wire in self._wires: - if not ignore: - if self._is_wire_idle(wire): - yield wire - else: - for node in self.nodes_on_wire(wire, only_ops=True): - if node.op.name not in ignore_set: - # If we found an op node outside of ignore we can stop iterating over the wire - break - else: - yield wire - - def size(self, *, recurse: bool = False): - """Return the number of operations. If there is control flow present, this count may only - be an estimate, as the complete control-flow path cannot be statically known. - - Args: - recurse: if ``True``, then recurse into control-flow operations. For loops with - known-length iterators are counted unrolled. If-else blocks sum both of the two - branches. While loops are counted as if the loop body runs once only. Defaults to - ``False`` and raises :class:`.DAGCircuitError` if any control flow is present, to - avoid silently returning a mostly meaningless number. - - Returns: - int: the circuit size - - Raises: - DAGCircuitError: if an unknown :class:`.ControlFlowOp` is present in a call with - ``recurse=True``, or any control flow is present in a non-recursive call. - """ - length = len(self._multi_graph) - 2 * len(self._wires) - if not recurse: - if any(x in self._op_names for x in CONTROL_FLOW_OP_NAMES): - raise DAGCircuitError( - "Size with control flow is ambiguous." - " You may use `recurse=True` to get a result," - " but see this method's documentation for the meaning of this." - ) - return length - # pylint: disable=cyclic-import - from qiskit.converters import circuit_to_dag - - for node in self.op_nodes(ControlFlowOp): - if isinstance(node.op, ForLoopOp): - indexset = node.op.params[0] - inner = len(indexset) * circuit_to_dag(node.op.blocks[0]).size(recurse=True) - elif isinstance(node.op, WhileLoopOp): - inner = circuit_to_dag(node.op.blocks[0]).size(recurse=True) - elif isinstance(node.op, (IfElseOp, SwitchCaseOp)): - inner = sum(circuit_to_dag(block).size(recurse=True) for block in node.op.blocks) - else: - raise DAGCircuitError(f"unknown control-flow type: '{node.op.name}'") - # Replace the "1" for the node itself with the actual count. - length += inner - 1 - return length - - def depth(self, *, recurse: bool = False): - """Return the circuit depth. If there is control flow present, this count may only be an - estimate, as the complete control-flow path cannot be statically known. - - Args: - recurse: if ``True``, then recurse into control-flow operations. For loops - with known-length iterators are counted as if the loop had been manually unrolled - (*i.e.* with each iteration of the loop body written out explicitly). - If-else blocks take the longer case of the two branches. While loops are counted as - if the loop body runs once only. Defaults to ``False`` and raises - :class:`.DAGCircuitError` if any control flow is present, to avoid silently - returning a nonsensical number. - - Returns: - int: the circuit depth - - Raises: - DAGCircuitError: if not a directed acyclic graph - DAGCircuitError: if unknown control flow is present in a recursive call, or any control - flow is present in a non-recursive call. - """ - if recurse: - from qiskit.converters import circuit_to_dag # pylint: disable=cyclic-import - - node_lookup = {} - for node in self.op_nodes(ControlFlowOp): - weight = len(node.op.params[0]) if isinstance(node.op, ForLoopOp) else 1 - if weight == 0: - node_lookup[node._node_id] = 0 - else: - node_lookup[node._node_id] = weight * max( - circuit_to_dag(block).depth(recurse=True) for block in node.op.blocks - ) - - def weight_fn(_source, target, _edge): - return node_lookup.get(target, 1) - - else: - if any(x in self._op_names for x in CONTROL_FLOW_OP_NAMES): - raise DAGCircuitError( - "Depth with control flow is ambiguous." - " You may use `recurse=True` to get a result," - " but see this method's documentation for the meaning of this." - ) - weight_fn = None - - try: - depth = rx.dag_longest_path_length(self._multi_graph, weight_fn) - 1 - except rx.DAGHasCycle as ex: - raise DAGCircuitError("not a DAG") from ex - return depth if depth >= 0 else 0 - - def width(self): - """Return the total number of qubits + clbits used by the circuit. - This function formerly returned the number of qubits by the calculation - return len(self._wires) - self.num_clbits() - but was changed by issue #2564 to return number of qubits + clbits - with the new function DAGCircuit.num_qubits replacing the former - semantic of DAGCircuit.width(). - """ - return len(self._wires) - - def num_qubits(self): - """Return the total number of qubits used by the circuit. - num_qubits() replaces former use of width(). - DAGCircuit.width() now returns qubits + clbits for - consistency with Circuit.width() [qiskit-terra #2564]. - """ - return len(self.qubits) - - def num_clbits(self): - """Return the total number of classical bits used by the circuit.""" - return len(self.clbits) - - def num_tensor_factors(self): - """Compute how many components the circuit can decompose into.""" - return rx.number_weakly_connected_components(self._multi_graph) - - @property - def num_vars(self): - """Total number of classical variables tracked by the circuit.""" - return len(self._vars_info) - - @property - def num_input_vars(self): - """Number of input classical variables tracked by the circuit.""" - return len(self._vars_by_type[_DAGVarType.INPUT]) - - @property - def num_captured_vars(self): - """Number of captured classical variables tracked by the circuit.""" - return len(self._vars_by_type[_DAGVarType.CAPTURE]) - - @property - def num_declared_vars(self): - """Number of declared local classical variables tracked by the circuit.""" - return len(self._vars_by_type[_DAGVarType.DECLARE]) - - def iter_vars(self): - """Iterable over all the classical variables tracked by the circuit.""" - return itertools.chain.from_iterable(self._vars_by_type.values()) - - def iter_input_vars(self): - """Iterable over the input classical variables tracked by the circuit.""" - return iter(self._vars_by_type[_DAGVarType.INPUT]) - - def iter_captured_vars(self): - """Iterable over the captured classical variables tracked by the circuit.""" - return iter(self._vars_by_type[_DAGVarType.CAPTURE]) - - def iter_declared_vars(self): - """Iterable over the declared local classical variables tracked by the circuit.""" - return iter(self._vars_by_type[_DAGVarType.DECLARE]) - - def has_var(self, var: str | expr.Var) -> bool: - """Is this realtime variable in the DAG? - - Args: - var: the variable or name to check. - """ - if isinstance(var, str): - return var in self._vars_info - return (info := self._vars_info.get(var.name, False)) and info.var is var - - def __eq__(self, other): - # Try to convert to float, but in case of unbound ParameterExpressions - # a TypeError will be raise, fallback to normal equality in those - # cases - try: - self_phase = float(self.global_phase) - other_phase = float(other.global_phase) - if ( - abs((self_phase - other_phase + np.pi) % (2 * np.pi) - np.pi) > 1.0e-10 - ): # TODO: atol? - return False - except TypeError: - if self.global_phase != other.global_phase: - return False - if self.calibrations != other.calibrations: - return False - - # We don't do any semantic equivalence between Var nodes, as things stand; DAGs can only be - # equal in our mind if they use the exact same UUID vars. - if self._vars_by_type != other._vars_by_type: - return False - - self_bit_indices = {bit: idx for idx, bit in enumerate(self.qubits + self.clbits)} - other_bit_indices = {bit: idx for idx, bit in enumerate(other.qubits + other.clbits)} - - self_qreg_indices = { - regname: [self_bit_indices[bit] for bit in reg] for regname, reg in self.qregs.items() - } - self_creg_indices = { - regname: [self_bit_indices[bit] for bit in reg] for regname, reg in self.cregs.items() - } - - other_qreg_indices = { - regname: [other_bit_indices[bit] for bit in reg] for regname, reg in other.qregs.items() - } - other_creg_indices = { - regname: [other_bit_indices[bit] for bit in reg] for regname, reg in other.cregs.items() - } - if self_qreg_indices != other_qreg_indices or self_creg_indices != other_creg_indices: - return False - - def node_eq(node_self, node_other): - return DAGNode.semantic_eq(node_self, node_other, self_bit_indices, other_bit_indices) - - return rx.is_isomorphic_node_match(self._multi_graph, other._multi_graph, node_eq) - - def topological_nodes(self, key=None): - """ - Yield nodes in topological order. - - Args: - key (Callable): A callable which will take a DAGNode object and - return a string sort key. If not specified the - :attr:`~qiskit.dagcircuit.DAGNode.sort_key` attribute will be - used as the sort key for each node. - - Returns: - generator(DAGOpNode, DAGInNode, or DAGOutNode): node in topological order - """ - - def _key(x): - return x.sort_key - - if key is None: - key = _key - - return iter(rx.lexicographical_topological_sort(self._multi_graph, key=key)) - - def topological_op_nodes(self, key: Callable | None = None) -> Generator[DAGOpNode, Any, Any]: - """ - Yield op nodes in topological order. - - Allowed to pass in specific key to break ties in top order - - Args: - key (Callable): A callable which will take a DAGNode object and - return a string sort key. If not specified the - :attr:`~qiskit.dagcircuit.DAGNode.sort_key` attribute will be - used as the sort key for each node. - - Returns: - generator(DAGOpNode): op node in topological order - """ - return (nd for nd in self.topological_nodes(key) if isinstance(nd, DAGOpNode)) - - def replace_block_with_op( - self, node_block: list[DAGOpNode], op: Operation, wire_pos_map, cycle_check=True - ): - """Replace a block of nodes with a single node. - - This is used to consolidate a block of DAGOpNodes into a single - operation. A typical example is a block of gates being consolidated - into a single ``UnitaryGate`` representing the unitary matrix of the - block. - - Args: - node_block (List[DAGNode]): A list of dag nodes that represents the - node block to be replaced - op (qiskit.circuit.Operation): The operation to replace the - block with - wire_pos_map (Dict[Bit, int]): The dictionary mapping the bits to their positions in the - output ``qargs`` or ``cargs``. This is necessary to reconstruct the arg order over - multiple gates in the combined single op node. If a :class:`.Bit` is not in the - dictionary, it will not be added to the args; this can be useful when dealing with - control-flow operations that have inherent bits in their ``condition`` or ``target`` - fields. :class:`.expr.Var` wires similarly do not need to be in this map, since - they will never be in ``qargs`` or ``cargs``. - cycle_check (bool): When set to True this method will check that - replacing the provided ``node_block`` with a single node - would introduce a cycle (which would invalidate the - ``DAGCircuit``) and will raise a ``DAGCircuitError`` if a cycle - would be introduced. This checking comes with a run time - penalty. If you can guarantee that your input ``node_block`` is - a contiguous block and won't introduce a cycle when it's - contracted to a single node, this can be set to ``False`` to - improve the runtime performance of this method. - - Raises: - DAGCircuitError: if ``cycle_check`` is set to ``True`` and replacing - the specified block introduces a cycle or if ``node_block`` is - empty. - - Returns: - DAGOpNode: The op node that replaces the block. - """ - block_qargs = set() - block_cargs = set() - block_ids = [x._node_id for x in node_block] - - # If node block is empty return early - if not node_block: - raise DAGCircuitError("Can't replace an empty node_block") - - for nd in node_block: - block_qargs |= set(nd.qargs) - block_cargs |= set(nd.cargs) - if (condition := getattr(nd, "condition", None)) is not None: - block_cargs.update(condition_resources(condition).clbits) - elif nd.name in CONTROL_FLOW_OP_NAMES and isinstance(nd.op, SwitchCaseOp): - if isinstance(nd.op.target, Clbit): - block_cargs.add(nd.op.target) - elif isinstance(nd.op.target, ClassicalRegister): - block_cargs.update(nd.op.target) - else: - block_cargs.update(node_resources(nd.op.target).clbits) - - block_qargs = [bit for bit in block_qargs if bit in wire_pos_map] - block_qargs.sort(key=wire_pos_map.get) - block_cargs = [bit for bit in block_cargs if bit in wire_pos_map] - block_cargs.sort(key=wire_pos_map.get) - new_node = DAGOpNode(op, block_qargs, block_cargs, dag=self) - - # check the op to insert matches the number of qubits we put it on - if op.num_qubits != len(block_qargs): - raise DAGCircuitError( - f"Number of qubits in the replacement operation ({op.num_qubits}) is not equal to " - f"the number of qubits in the block ({len(block_qargs)})!" - ) - - try: - new_node._node_id = self._multi_graph.contract_nodes( - block_ids, new_node, check_cycle=cycle_check - ) - except rx.DAGWouldCycle as ex: - raise DAGCircuitError( - "Replacing the specified node block would introduce a cycle" - ) from ex - - self._increment_op(op.name) - - for nd in node_block: - self._decrement_op(nd.name) - - return new_node - - def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condition=True): - """Replace one node with dag. - - Args: - node (DAGOpNode): node to substitute - input_dag (DAGCircuit): circuit that will substitute the node. - wires (list[Bit] | Dict[Bit, Bit]): gives an order for (qu)bits - in the input circuit. If a list, then the bits refer to those in the ``input_dag``, - and the order gets matched to the node wires by qargs first, then cargs, then - conditions. If a dictionary, then a mapping of bits in the ``input_dag`` to those - that the ``node`` acts on. - - Standalone :class:`~.expr.Var` nodes cannot currently be remapped as part of the - substitution; the ``input_dag`` should be defined over the correct set of variables - already. - - .. - The rule about not remapping `Var`s is to avoid performance pitfalls and reduce - complexity; the creator of the input DAG should easily be able to arrange for - the correct `Var`s to be used, and doing so avoids us needing to recurse through - control-flow operations to do deep remappings. - propagate_condition (bool): If ``True`` (default), then any ``condition`` attribute on - the operation within ``node`` is propagated to each node in the ``input_dag``. If - ``False``, then the ``input_dag`` is assumed to faithfully implement suitable - conditional logic already. This is ignored for :class:`.ControlFlowOp`\\ s (i.e. - treated as if it is ``False``); replacements of those must already fulfill the same - conditional logic or this function would be close to useless for them. - - Returns: - dict: maps node IDs from `input_dag` to their new node incarnations in `self`. - - Raises: - DAGCircuitError: if met with unexpected predecessor/successors - """ - if not isinstance(node, DAGOpNode): - raise DAGCircuitError(f"expected node DAGOpNode, got {type(node)}") - - if isinstance(wires, dict): - wire_map = wires - else: - wires = input_dag.wires if wires is None else wires - node_cargs = set(node.cargs) - node_wire_order = list(node.qargs) + list(node.cargs) - # If we're not propagating it, the number of wires in the input DAG should include the - # condition as well. - if not propagate_condition and _may_have_additional_wires(node): - node_wire_order += [ - wire for wire in _additional_wires(node.op) if wire not in node_cargs - ] - if len(wires) != len(node_wire_order): - raise DAGCircuitError( - f"bit mapping invalid: expected {len(node_wire_order)}, got {len(wires)}" - ) - wire_map = dict(zip(wires, node_wire_order)) - if len(wire_map) != len(node_wire_order): - raise DAGCircuitError("bit mapping invalid: some bits have duplicate entries") - for input_dag_wire, our_wire in wire_map.items(): - if our_wire not in self.input_map: - raise DAGCircuitError(f"bit mapping invalid: {our_wire} is not in this DAG") - if isinstance(our_wire, expr.Var) or isinstance(input_dag_wire, expr.Var): - raise DAGCircuitError("`Var` nodes cannot be remapped during substitution") - # Support mapping indiscriminately between Qubit and AncillaQubit, etc. - check_type = Qubit if isinstance(our_wire, Qubit) else Clbit - if not isinstance(input_dag_wire, check_type): - raise DAGCircuitError( - f"bit mapping invalid: {input_dag_wire} and {our_wire} are different bit types" - ) - if _may_have_additional_wires(node): - node_vars = {var for var in _additional_wires(node.op) if isinstance(var, expr.Var)} - else: - node_vars = set() - dag_vars = set(input_dag.iter_vars()) - if dag_vars - node_vars: - raise DAGCircuitError( - "Cannot replace a node with a DAG with more variables." - f" Variables in node: {node_vars}." - f" Variables in DAG: {dag_vars}." - ) - for var in dag_vars: - wire_map[var] = var - - reverse_wire_map = {b: a for a, b in wire_map.items()} - # It doesn't make sense to try and propagate a condition from a control-flow op; a - # replacement for the control-flow op should implement the operation completely. - if propagate_condition and not node.is_control_flow() and node.condition is not None: - in_dag = input_dag.copy_empty_like() - # The remapping of `condition` below is still using the old code that assumes a 2-tuple. - # This is because this remapping code only makes sense in the case of non-control-flow - # operations being replaced. These can only have the 2-tuple conditions, and the - # ability to set a condition at an individual node level will be deprecated and removed - # in favour of the new-style conditional blocks. The extra logic in here to add - # additional wires into the map as necessary would hugely complicate matters if we tried - # to abstract it out into the `VariableMapper` used elsewhere. - target, value = node.condition - if isinstance(target, Clbit): - new_target = reverse_wire_map.get(target, Clbit()) - if new_target not in wire_map: - in_dag.add_clbits([new_target]) - wire_map[new_target], reverse_wire_map[target] = target, new_target - target_cargs = {new_target} - else: # ClassicalRegister - mapped_bits = [reverse_wire_map.get(bit, Clbit()) for bit in target] - for ours, theirs in zip(target, mapped_bits): - # Update to any new dummy bits we just created to the wire maps. - wire_map[theirs], reverse_wire_map[ours] = ours, theirs - new_target = ClassicalRegister(bits=mapped_bits) - in_dag.add_creg(new_target) - target_cargs = set(new_target) - new_condition = (new_target, value) - for in_node in input_dag.topological_op_nodes(): - if getattr(in_node.op, "condition", None) is not None: - raise DAGCircuitError( - "cannot propagate a condition to an element that already has one" - ) - if target_cargs.intersection(in_node.cargs): - # This is for backwards compatibility with early versions of the method, as it is - # a tested part of the API. In the newer model of a condition being an integral - # part of the operation (not a separate property to be copied over), this error - # is overzealous, because it forbids a custom instruction from implementing the - # condition within its definition rather than at the top level. - raise DAGCircuitError( - "cannot propagate a condition to an element that acts on those bits" - ) - new_op = copy.copy(in_node.op) - if new_condition: - if not isinstance(new_op, ControlFlowOp): - new_op = new_op.c_if(*new_condition) - else: - new_op.condition = new_condition - in_dag.apply_operation_back(new_op, in_node.qargs, in_node.cargs, check=False) - else: - in_dag = input_dag - - if in_dag.global_phase: - self.global_phase += in_dag.global_phase - - # Add wire from pred to succ if no ops on mapped wire on ``in_dag`` - # rustworkx's substitute_node_with_subgraph lacks the DAGCircuit - # context to know what to do in this case (the method won't even see - # these nodes because they're filtered) so we manually retain the - # edges prior to calling substitute_node_with_subgraph and set the - # edge_map_fn callback kwarg to skip these edges when they're - # encountered. - for in_dag_wire, self_wire in wire_map.items(): - input_node = in_dag.input_map[in_dag_wire] - output_node = in_dag.output_map[in_dag_wire] - if in_dag._multi_graph.has_edge(input_node._node_id, output_node._node_id): - pred = self._multi_graph.find_predecessors_by_edge( - node._node_id, lambda edge, wire=self_wire: edge == wire - )[0] - succ = self._multi_graph.find_successors_by_edge( - node._node_id, lambda edge, wire=self_wire: edge == wire - )[0] - self._multi_graph.add_edge(pred._node_id, succ._node_id, self_wire) - for contracted_var in node_vars - dag_vars: - pred = self._multi_graph.find_predecessors_by_edge( - node._node_id, lambda edge, wire=contracted_var: edge == wire - )[0] - succ = self._multi_graph.find_successors_by_edge( - node._node_id, lambda edge, wire=contracted_var: edge == wire - )[0] - self._multi_graph.add_edge(pred._node_id, succ._node_id, contracted_var) - - # Exclude any nodes from in_dag that are not a DAGOpNode or are on - # wires outside the set specified by the wires kwarg - def filter_fn(node): - if not isinstance(node, DAGOpNode): - return False - for _, _, wire in in_dag.edges(node): - if wire not in wire_map: - return False - return True - - # Map edges into and out of node to the appropriate node from in_dag - def edge_map_fn(source, _target, self_wire): - wire = reverse_wire_map[self_wire] - # successor edge - if source == node._node_id: - wire_output_id = in_dag.output_map[wire]._node_id - out_index = in_dag._multi_graph.predecessor_indices(wire_output_id)[0] - # Edge directly from from input nodes to output nodes in in_dag are - # already handled prior to calling rustworkx. Don't map these edges - # in rustworkx. - if not isinstance(in_dag._multi_graph[out_index], DAGOpNode): - return None - # predecessor edge - else: - wire_input_id = in_dag.input_map[wire]._node_id - out_index = in_dag._multi_graph.successor_indices(wire_input_id)[0] - # Edge directly from from input nodes to output nodes in in_dag are - # already handled prior to calling rustworkx. Don't map these edges - # in rustworkx. - if not isinstance(in_dag._multi_graph[out_index], DAGOpNode): - return None - return out_index - - # Adjust edge weights from in_dag - def edge_weight_map(wire): - return wire_map[wire] - - node_map = self._multi_graph.substitute_node_with_subgraph( - node._node_id, in_dag._multi_graph, edge_map_fn, filter_fn, edge_weight_map - ) - self._decrement_op(node.name) - - variable_mapper = _classical_resource_map.VariableMapper( - self.cregs.values(), wire_map, add_register=self.add_creg - ) - # Iterate over nodes of input_circuit and update wires in node objects migrated - # from in_dag - for old_node_index, new_node_index in node_map.items(): - # update node attributes - old_node = in_dag._multi_graph[old_node_index] - m_op = None - if not old_node.is_standard_gate() and isinstance(old_node.op, SwitchCaseOp): - m_op = SwitchCaseOp( - variable_mapper.map_target(old_node.op.target), - old_node.op.cases_specifier(), - label=old_node.op.label, - ) - elif old_node.condition is not None: - m_op = old_node.op - if old_node.is_control_flow(): - m_op.condition = variable_mapper.map_condition(m_op.condition) - else: - new_condition = variable_mapper.map_condition(m_op.condition) - if new_condition is not None: - m_op = m_op.c_if(*new_condition) - m_qargs = [wire_map[x] for x in old_node.qargs] - m_cargs = [wire_map[x] for x in old_node.cargs] - old_instruction = old_node._to_circuit_instruction() - if m_op is None: - new_instruction = old_instruction.replace(qubits=m_qargs, clbits=m_cargs) - else: - new_instruction = old_instruction.replace( - operation=m_op, qubits=m_qargs, clbits=m_cargs - ) - new_node = DAGOpNode.from_instruction(new_instruction) - new_node._node_id = new_node_index - self._multi_graph[new_node_index] = new_node - self._increment_op(new_node.name) - - return {k: self._multi_graph[v] for k, v in node_map.items()} - - def substitute_node(self, node: DAGOpNode, op, inplace: bool = False, propagate_condition=True): - """Replace an DAGOpNode with a single operation. qargs, cargs and - conditions for the new operation will be inferred from the node to be - replaced. The new operation will be checked to match the shape of the - replaced operation. - - Args: - node (DAGOpNode): Node to be replaced - op (qiskit.circuit.Operation): The :class:`qiskit.circuit.Operation` - instance to be added to the DAG - inplace (bool): Optional, default False. If True, existing DAG node - will be modified to include op. Otherwise, a new DAG node will - be used. - propagate_condition (bool): Optional, default True. If True, a condition on the - ``node`` to be replaced will be applied to the new ``op``. This is the legacy - behavior. If either node is a control-flow operation, this will be ignored. If - the ``op`` already has a condition, :exc:`.DAGCircuitError` is raised. - - Returns: - DAGOpNode: the new node containing the added operation. - - Raises: - DAGCircuitError: If replacement operation was incompatible with - location of target node. - """ - - if not isinstance(node, DAGOpNode): - raise DAGCircuitError("Only DAGOpNodes can be replaced.") - - if node.op.num_qubits != op.num_qubits or node.op.num_clbits != op.num_clbits: - raise DAGCircuitError( - f"Cannot replace node of width ({node.op.num_qubits} qubits, " - f"{node.op.num_clbits} clbits) with " - f"operation of mismatched width ({op.num_qubits} qubits, " - f"{op.num_clbits} clbits)." - ) - - # This might include wires that are inherent to the node, like in its `condition` or - # `target` fields, so might be wider than `node.op.num_{qu,cl}bits`. - current_wires = {wire for _, _, wire in self.edges(node)} - new_wires = set(node.qargs) | set(node.cargs) | set(_additional_wires(op)) - - if propagate_condition and not ( - isinstance(node.op, ControlFlowOp) or isinstance(op, ControlFlowOp) - ): - if getattr(op, "condition", None) is not None: - raise DAGCircuitError( - "Cannot propagate a condition to an operation that already has one." - ) - if (old_condition := getattr(node.op, "condition", None)) is not None: - if not isinstance(op, Instruction): - raise DAGCircuitError("Cannot add a condition on a generic Operation.") - if not isinstance(node.op, ControlFlowOp): - op = op.c_if(*old_condition) - else: - op.condition = old_condition - new_wires.update(condition_resources(old_condition).clbits) - - if new_wires != current_wires: - # The new wires must be a non-strict subset of the current wires; if they add new wires, - # we'd not know where to cut the existing wire to insert the new dependency. - raise DAGCircuitError( - f"New operation '{op}' does not span the same wires as the old node '{node}'." - f" New wires: {new_wires}, old wires: {current_wires}." - ) - - if inplace: - if op.name != node.op.name: - self._increment_op(op.name) - self._decrement_op(node.name) - node.op = op - return node - - new_node = copy.copy(node) - new_node.op = op - self._multi_graph[node._node_id] = new_node - if op.name != node.name: - self._increment_op(op.name) - self._decrement_op(node.name) - return new_node - - def separable_circuits( - self, remove_idle_qubits: bool = False, *, vars_mode: _VarsMode = "alike" - ) -> list["DAGCircuit"]: - """Decompose the circuit into sets of qubits with no gates connecting them. - - Args: - remove_idle_qubits (bool): Flag denoting whether to remove idle qubits from - the separated circuits. If ``False``, each output circuit will contain the - same number of qubits as ``self``. - vars_mode: how any realtime :class:`~.expr.Var` nodes should be handled in the output - DAGs. See :meth:`copy_empty_like` for details on the modes. - - Returns: - List[DAGCircuit]: The circuits resulting from separating ``self`` into sets - of disconnected qubits - - Each :class:`~.DAGCircuit` instance returned by this method will contain the same number of - clbits as ``self``. The global phase information in ``self`` will not be maintained - in the subcircuits returned by this method. - """ - connected_components = rx.weakly_connected_components(self._multi_graph) - - # Collect each disconnected subgraph - disconnected_subgraphs = [] - for components in connected_components: - disconnected_subgraphs.append(self._multi_graph.subgraph(list(components))) - - # Helper function for ensuring rustworkx nodes are returned in lexicographical, - # topological order - def _key(x): - return x.sort_key - - # Create new DAGCircuit objects from each of the rustworkx subgraph objects - decomposed_dags = [] - for subgraph in disconnected_subgraphs: - new_dag = self.copy_empty_like(vars_mode=vars_mode) - new_dag.global_phase = 0 - subgraph_is_classical = True - for node in rx.lexicographical_topological_sort(subgraph, key=_key): - if isinstance(node, DAGInNode): - if isinstance(node.wire, Qubit): - subgraph_is_classical = False - if not isinstance(node, DAGOpNode): - continue - new_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - - # Ignore DAGs created for empty clbits - if not subgraph_is_classical: - decomposed_dags.append(new_dag) - - if remove_idle_qubits: - for dag in decomposed_dags: - dag.remove_qubits(*(bit for bit in dag.idle_wires() if isinstance(bit, Qubit))) - - return decomposed_dags - - def swap_nodes(self, node1, node2): - """Swap connected nodes e.g. due to commutation. - - Args: - node1 (OpNode): predecessor node - node2 (OpNode): successor node - - Raises: - DAGCircuitError: if either node is not an OpNode or nodes are not connected - """ - if not (isinstance(node1, DAGOpNode) and isinstance(node2, DAGOpNode)): - raise DAGCircuitError("nodes to swap are not both DAGOpNodes") - try: - connected_edges = self._multi_graph.get_all_edge_data(node1._node_id, node2._node_id) - except rx.NoEdgeBetweenNodes as no_common_edge: - raise DAGCircuitError("attempt to swap unconnected nodes") from no_common_edge - node1_id = node1._node_id - node2_id = node2._node_id - for edge in connected_edges[::-1]: - edge_find = lambda x, y=edge: x == y - edge_parent = self._multi_graph.find_predecessors_by_edge(node1_id, edge_find)[0] - self._multi_graph.remove_edge(edge_parent._node_id, node1_id) - self._multi_graph.add_edge(edge_parent._node_id, node2_id, edge) - edge_child = self._multi_graph.find_successors_by_edge(node2_id, edge_find)[0] - self._multi_graph.remove_edge(node1_id, node2_id) - self._multi_graph.add_edge(node2_id, node1_id, edge) - self._multi_graph.remove_edge(node2_id, edge_child._node_id) - self._multi_graph.add_edge(node1_id, edge_child._node_id, edge) - - def node(self, node_id): - """Get the node in the dag. - - Args: - node_id(int): Node identifier. - - Returns: - node: the node. - """ - return self._multi_graph[node_id] - - def nodes(self): - """Iterator for node values. - - Yield: - node: the node. - """ - yield from self._multi_graph.nodes() - - def edges(self, nodes=None): - """Iterator for edge values and source and dest node - - This works by returning the output edges from the specified nodes. If - no nodes are specified all edges from the graph are returned. - - Args: - nodes(DAGOpNode, DAGInNode, or DAGOutNode|list(DAGOpNode, DAGInNode, or DAGOutNode): - Either a list of nodes or a single input node. If none is specified, - all edges are returned from the graph. - - Yield: - edge: the edge in the same format as out_edges the tuple - (source node, destination node, edge data) - """ - if nodes is None: - nodes = self._multi_graph.nodes() - - elif isinstance(nodes, (DAGOpNode, DAGInNode, DAGOutNode)): - nodes = [nodes] - for node in nodes: - raw_nodes = self._multi_graph.out_edges(node._node_id) - for source, dest, edge in raw_nodes: - yield (self._multi_graph[source], self._multi_graph[dest], edge) - - def op_nodes(self, op=None, include_directives=True): - """Get the list of "op" nodes in the dag. - - Args: - op (Type): :class:`qiskit.circuit.Operation` subclass op nodes to - return. If None, return all op nodes. - include_directives (bool): include `barrier`, `snapshot` etc. - - Returns: - list[DAGOpNode]: the list of node ids containing the given op. - """ - nodes = [] - filter_is_nonstandard = getattr(op, "_standard_gate", None) is None - for node in self._multi_graph.nodes(): - if isinstance(node, DAGOpNode): - if not include_directives and node.is_directive(): - continue - if op is None or ( - # This middle catch is to avoid Python-space operation creation for most uses of - # `op`; we're usually just looking for control-flow ops, and standard gates - # aren't control-flow ops. - not (filter_is_nonstandard and node.is_standard_gate()) - and isinstance(node.op, op) - ): - nodes.append(node) - return nodes - - def gate_nodes(self): - """Get the list of gate nodes in the dag. - - Returns: - list[DAGOpNode]: the list of DAGOpNodes that represent gates. - """ - nodes = [] - for node in self.op_nodes(): - if isinstance(node.op, Gate): - nodes.append(node) - return nodes - - def named_nodes(self, *names): - """Get the set of "op" nodes with the given name.""" - named_nodes = [] - for node in self._multi_graph.nodes(): - if isinstance(node, DAGOpNode) and node.name in names: - named_nodes.append(node) - return named_nodes - - def two_qubit_ops(self): - """Get list of 2 qubit operations. Ignore directives like snapshot and barrier.""" - ops = [] - for node in self.op_nodes(include_directives=False): - if len(node.qargs) == 2: - ops.append(node) - return ops - - def multi_qubit_ops(self): - """Get list of 3+ qubit operations. Ignore directives like snapshot and barrier.""" - ops = [] - for node in self.op_nodes(include_directives=False): - if len(node.qargs) >= 3: - ops.append(node) - return ops - - def longest_path(self): - """Returns the longest path in the dag as a list of DAGOpNodes, DAGInNodes, and DAGOutNodes.""" - return [self._multi_graph[x] for x in rx.dag_longest_path(self._multi_graph)] - - def successors(self, node): - """Returns iterator of the successors of a node as DAGOpNodes and DAGOutNodes.""" - return iter(self._multi_graph.successors(node._node_id)) - - def predecessors(self, node): - """Returns iterator of the predecessors of a node as DAGOpNodes and DAGInNodes.""" - return iter(self._multi_graph.predecessors(node._node_id)) - - def op_successors(self, node): - """Returns iterator of "op" successors of a node in the dag.""" - return (succ for succ in self.successors(node) if isinstance(succ, DAGOpNode)) - - def op_predecessors(self, node): - """Returns the iterator of "op" predecessors of a node in the dag.""" - return (pred for pred in self.predecessors(node) if isinstance(pred, DAGOpNode)) - - def is_successor(self, node, node_succ): - """Checks if a second node is in the successors of node.""" - return self._multi_graph.has_edge(node._node_id, node_succ._node_id) - - def is_predecessor(self, node, node_pred): - """Checks if a second node is in the predecessors of node.""" - return self._multi_graph.has_edge(node_pred._node_id, node._node_id) - - def quantum_predecessors(self, node): - """Returns iterator of the predecessors of a node that are - connected by a quantum edge as DAGOpNodes and DAGInNodes.""" - return iter( - self._multi_graph.find_predecessors_by_edge( - node._node_id, lambda edge_data: isinstance(edge_data, Qubit) - ) - ) - - def classical_predecessors(self, node): - """Returns iterator of the predecessors of a node that are - connected by a classical edge as DAGOpNodes and DAGInNodes.""" - return iter( - self._multi_graph.find_predecessors_by_edge( - node._node_id, lambda edge_data: not isinstance(edge_data, Qubit) - ) - ) - - def ancestors(self, node): - """Returns set of the ancestors of a node as DAGOpNodes and DAGInNodes.""" - return {self._multi_graph[x] for x in rx.ancestors(self._multi_graph, node._node_id)} - - def descendants(self, node): - """Returns set of the descendants of a node as DAGOpNodes and DAGOutNodes.""" - return {self._multi_graph[x] for x in rx.descendants(self._multi_graph, node._node_id)} - - def bfs_successors(self, node): - """ - Returns an iterator of tuples of (DAGNode, [DAGNodes]) where the DAGNode is the current node - and [DAGNode] is its successors in BFS order. - """ - return iter(rx.bfs_successors(self._multi_graph, node._node_id)) - - def quantum_successors(self, node): - """Returns iterator of the successors of a node that are - connected by a quantum edge as Opnodes and DAGOutNodes.""" - return iter( - self._multi_graph.find_successors_by_edge( - node._node_id, lambda edge_data: isinstance(edge_data, Qubit) - ) - ) - - def classical_successors(self, node): - """Returns iterator of the successors of a node that are - connected by a classical edge as DAGOpNodes and DAGInNodes.""" - return iter( - self._multi_graph.find_successors_by_edge( - node._node_id, lambda edge_data: not isinstance(edge_data, Qubit) - ) - ) - - def remove_op_node(self, node): - """Remove an operation node n. - - Add edges from predecessors to successors. - """ - if not isinstance(node, DAGOpNode): - raise DAGCircuitError( - f'The method remove_op_node only works on DAGOpNodes. A "{type(node)}" ' - "node type was wrongly provided." - ) - - self._multi_graph.remove_node_retain_edges_by_id(node._node_id) - self._decrement_op(node.name) - - def remove_ancestors_of(self, node): - """Remove all of the ancestor operation nodes of node.""" - anc = rx.ancestors(self._multi_graph, node) - # TODO: probably better to do all at once using - # multi_graph.remove_nodes_from; same for related functions ... - - for anc_node in anc: - if isinstance(anc_node, DAGOpNode): - self.remove_op_node(anc_node) - - def remove_descendants_of(self, node): - """Remove all of the descendant operation nodes of node.""" - desc = rx.descendants(self._multi_graph, node) - for desc_node in desc: - if isinstance(desc_node, DAGOpNode): - self.remove_op_node(desc_node) - - def remove_nonancestors_of(self, node): - """Remove all of the non-ancestors operation nodes of node.""" - anc = rx.ancestors(self._multi_graph, node) - comp = list(set(self._multi_graph.nodes()) - set(anc)) - for n in comp: - if isinstance(n, DAGOpNode): - self.remove_op_node(n) - - def remove_nondescendants_of(self, node): - """Remove all of the non-descendants operation nodes of node.""" - dec = rx.descendants(self._multi_graph, node) - comp = list(set(self._multi_graph.nodes()) - set(dec)) - for n in comp: - if isinstance(n, DAGOpNode): - self.remove_op_node(n) - - def front_layer(self): - """Return a list of op nodes in the first layer of this dag.""" - graph_layers = self.multigraph_layers() - try: - next(graph_layers) # Remove input nodes - except StopIteration: - return [] - - op_nodes = [node for node in next(graph_layers) if isinstance(node, DAGOpNode)] - - return op_nodes - - def layers(self, *, vars_mode: _VarsMode = "captures"): - """Yield a shallow view on a layer of this DAGCircuit for all d layers of this circuit. - - A layer is a circuit whose gates act on disjoint qubits, i.e., - a layer has depth 1. The total number of layers equals the - circuit depth d. The layers are indexed from 0 to d-1 with the - earliest layer at index 0. The layers are constructed using a - greedy algorithm. Each returned layer is a dict containing - {"graph": circuit graph, "partition": list of qubit lists}. - - The returned layer contains new (but semantically equivalent) DAGOpNodes, DAGInNodes, - and DAGOutNodes. These are not the same as nodes of the original dag, but are equivalent - via DAGNode.semantic_eq(node1, node2). - - TODO: Gates that use the same cbits will end up in different - layers as this is currently implemented. This may not be - the desired behavior. - - Args: - vars_mode: how any realtime :class:`~.expr.Var` nodes should be handled in the output - DAGs. See :meth:`copy_empty_like` for details on the modes. - """ - graph_layers = self.multigraph_layers() - try: - next(graph_layers) # Remove input nodes - except StopIteration: - return - - for graph_layer in graph_layers: - - # Get the op nodes from the layer, removing any input and output nodes. - op_nodes = [node for node in graph_layer if isinstance(node, DAGOpNode)] - - # Sort to make sure they are in the order they were added to the original DAG - # It has to be done by node_id as graph_layer is just a list of nodes - # with no implied topology - # Drawing tools rely on _node_id to infer order of node creation - # so we need this to be preserved by layers() - op_nodes.sort(key=lambda nd: nd._node_id) - - # Stop yielding once there are no more op_nodes in a layer. - if not op_nodes: - return - - # Construct a shallow copy of self - new_layer = self.copy_empty_like(vars_mode=vars_mode) - - for node in op_nodes: - new_layer._apply_op_node_back(node, check=False) - - # The quantum registers that have an operation in this layer. - support_list = [ - op_node.qargs for op_node in new_layer.op_nodes() if not op_node.is_directive() - ] - - yield {"graph": new_layer, "partition": support_list} - - def serial_layers(self, *, vars_mode: _VarsMode = "captures"): - """Yield a layer for all gates of this circuit. - - A serial layer is a circuit with one gate. The layers have the - same structure as in layers(). - - Args: - vars_mode: how any realtime :class:`~.expr.Var` nodes should be handled in the output - DAGs. See :meth:`copy_empty_like` for details on the modes. - """ - for next_node in self.topological_op_nodes(): - new_layer = self.copy_empty_like(vars_mode=vars_mode) - - # Save the support of the operation we add to the layer - support_list = [] - # Operation data - op = copy.copy(next_node.op) - qargs = copy.copy(next_node.qargs) - cargs = copy.copy(next_node.cargs) - - # Add node to new_layer - new_layer.apply_operation_back(op, qargs, cargs, check=False) - # Add operation to partition - if not getattr(next_node.op, "_directive", False): - support_list.append(list(qargs)) - l_dict = {"graph": new_layer, "partition": support_list} - yield l_dict - - def multigraph_layers(self): - """Yield layers of the multigraph.""" - first_layer = [x._node_id for x in self.input_map.values()] - return iter(rx.layers(self._multi_graph, first_layer)) - - def collect_runs(self, namelist): - """Return a set of non-conditional runs of "op" nodes with the given names. - - For example, "... h q[0]; cx q[0],q[1]; cx q[0],q[1]; h q[1]; .." - would produce the tuple of cx nodes as an element of the set returned - from a call to collect_runs(["cx"]). If instead the cx nodes were - "cx q[0],q[1]; cx q[1],q[0];", the method would still return the - pair in a tuple. The namelist can contain names that are not - in the circuit's basis. - - Nodes must have only one successor to continue the run. - """ - - def filter_fn(node): - return isinstance(node, DAGOpNode) and node.name in namelist and node.condition is None - - group_list = rx.collect_runs(self._multi_graph, filter_fn) - return {tuple(x) for x in group_list} - - def collect_1q_runs(self) -> list[list[DAGOpNode]]: - """Return a set of non-conditional runs of 1q "op" nodes.""" - return rx.collect_runs(self._multi_graph, collect_1q_runs_filter) - - def collect_2q_runs(self): - """Return a set of non-conditional runs of 2q "op" nodes.""" - - def color_fn(edge): - if isinstance(edge, Qubit): - return self.find_bit(edge).index - else: - return None - - return rx.collect_bicolor_runs(self._multi_graph, collect_2q_blocks_filter, color_fn) - - def nodes_on_wire(self, wire, only_ops=False): - """ - Iterator for nodes that affect a given wire. - - Args: - wire (Bit): the wire to be looked at. - only_ops (bool): True if only the ops nodes are wanted; - otherwise, all nodes are returned. - Yield: - Iterator: the successive nodes on the given wire - - Raises: - DAGCircuitError: if the given wire doesn't exist in the DAG - """ - current_node = self.input_map.get(wire, None) - - if not current_node: - raise DAGCircuitError(f"The given wire {str(wire)} is not present in the circuit") - - more_nodes = True - while more_nodes: - more_nodes = False - # allow user to just get ops on the wire - not the input/output nodes - if isinstance(current_node, DAGOpNode) or not only_ops: - yield current_node - - try: - current_node = self._multi_graph.find_adjacent_node_by_edge( - current_node._node_id, lambda x: wire == x - ) - more_nodes = True - except rx.NoSuitableNeighbors: - pass - - def count_ops(self, *, recurse: bool = True): - """Count the occurrences of operation names. - - Args: - recurse: if ``True`` (default), then recurse into control-flow operations. In all - cases, this counts only the number of times the operation appears in any possible - block; both branches of if-elses are counted, and for- and while-loop blocks are - only counted once. - - Returns: - Mapping[str, int]: a mapping of operation names to the number of times it appears. - """ - if not recurse or not CONTROL_FLOW_OP_NAMES.intersection(self._op_names): - return self._op_names.copy() - - # pylint: disable=cyclic-import - from qiskit.converters import circuit_to_dag - - def inner(dag, counts): - for name, count in dag._op_names.items(): - counts[name] += count - for node in dag.op_nodes(ControlFlowOp): - for block in node.op.blocks: - counts = inner(circuit_to_dag(block), counts) - return counts - - return dict(inner(self, defaultdict(int))) - - def count_ops_longest_path(self): - """Count the occurrences of operation names on the longest path. - - Returns a dictionary of counts keyed on the operation name. - """ - op_dict = {} - path = self.longest_path() - path = path[1:-1] # remove qubits at beginning and end of path - for node in path: - name = node.op.name - if name not in op_dict: - op_dict[name] = 1 - else: - op_dict[name] += 1 - return op_dict - - def quantum_causal_cone(self, qubit): - """ - Returns causal cone of a qubit. - - A qubit's causal cone is the set of qubits that can influence the output of that - qubit through interactions, whether through multi-qubit gates or operations. Knowing - the causal cone of a qubit can be useful when debugging faulty circuits, as it can - help identify which wire(s) may be causing the problem. - - This method does not consider any classical data dependency in the ``DAGCircuit``, - classical bit wires are ignored for the purposes of building the causal cone. - - Args: - qubit (~qiskit.circuit.Qubit): The output qubit for which we want to find the causal cone. - - Returns: - Set[~qiskit.circuit.Qubit]: The set of qubits whose interactions affect ``qubit``. - """ - # Retrieve the output node from the qubit - output_node = self.output_map.get(qubit, None) - if not output_node: - raise DAGCircuitError(f"Qubit {qubit} is not part of this circuit.") - - qubits_in_cone = {qubit} - queue = deque(self.quantum_predecessors(output_node)) - - # The processed_non_directive_nodes stores the set of processed non-directive nodes. - # This is an optimization to avoid considering the same non-directive node multiple - # times when reached from different paths. - # The directive nodes (such as barriers or measures) are trickier since when processing - # them we only add their predecessors that intersect qubits_in_cone. Hence, directive - # nodes have to be considered multiple times. - processed_non_directive_nodes = set() - - while queue: - node_to_check = queue.popleft() - - if isinstance(node_to_check, DAGOpNode): - # If the operation is not a directive (in particular not a barrier nor a measure), - # we do not do anything if it was already processed. Otherwise, we add its qubits - # to qubits_in_cone, and append its predecessors to queue. - if not getattr(node_to_check.op, "_directive"): - if node_to_check in processed_non_directive_nodes: - continue - qubits_in_cone = qubits_in_cone.union(set(node_to_check.qargs)) - processed_non_directive_nodes.add(node_to_check) - for pred in self.quantum_predecessors(node_to_check): - if isinstance(pred, DAGOpNode): - queue.append(pred) - else: - # Directives (such as barriers and measures) may be defined over all the qubits, - # yet not all of these qubits should be considered in the causal cone. So we - # only add those predecessors that have qubits in common with qubits_in_cone. - for pred in self.quantum_predecessors(node_to_check): - if isinstance(pred, DAGOpNode) and not qubits_in_cone.isdisjoint( - set(pred.qargs) - ): - queue.append(pred) - - return qubits_in_cone - - def properties(self): - """Return a dictionary of circuit properties.""" - summary = { - "size": self.size(), - "depth": self.depth(), - "width": self.width(), - "qubits": self.num_qubits(), - "bits": self.num_clbits(), - "factors": self.num_tensor_factors(), - "operations": self.count_ops(), - } - return summary - - def draw(self, scale=0.7, filename=None, style="color"): - """ - Draws the dag circuit. - - This function needs `Graphviz `_ to be - installed. Graphviz is not a python package and can't be pip installed - (the ``graphviz`` package on PyPI is a Python interface library for - Graphviz and does not actually install Graphviz). You can refer to - `the Graphviz documentation `__ on - how to install it. - - Args: - scale (float): scaling factor - filename (str): file path to save image to (format inferred from name) - style (str): - 'plain': B&W graph; - 'color' (default): color input/output/op nodes - - Returns: - Ipython.display.Image: if in Jupyter notebook and not saving to file, - otherwise None. - """ - from qiskit.visualization.dag_visualization import dag_drawer - - return dag_drawer(dag=self, scale=scale, filename=filename, style=style) - - -class _DAGVarType(enum.Enum): - INPUT = enum.auto() - CAPTURE = enum.auto() - DECLARE = enum.auto() - - -class _DAGVarInfo: - __slots__ = ("var", "type", "in_node", "out_node") - - def __init__(self, var: expr.Var, type_: _DAGVarType, in_node: DAGInNode, out_node: DAGOutNode): - self.var = var - self.type = type_ - self.in_node = in_node - self.out_node = out_node - - -def _may_have_additional_wires(node) -> bool: - """Return whether a given :class:`.DAGOpNode` may contain references to additional wires - locations within its :class:`.Operation`. If this is ``True``, it doesn't necessarily mean - that the operation _will_ access memory inherently, but a ``False`` return guarantees that it - won't. - - The memory might be classical bits or classical variables, such as a control-flow operation or a - store. - - Args: - operation (qiskit.dagcircuit.DAGOpNode): the operation to check. - """ - # This is separate to `_additional_wires` because most of the time there won't be any extra - # wires beyond the explicit `qargs` and `cargs` so we want a fast path to be able to skip - # creating and testing a generator for emptiness. - # - # If updating this, you most likely also need to update `_additional_wires`. - return node.condition is not None or ( - not node.is_standard_gate() and isinstance(node.op, (ControlFlowOp, Store)) - ) - - -def _additional_wires(operation) -> Iterable[Clbit | expr.Var]: - """Return an iterable over the additional tracked memory usage in this operation. These - additional wires include (for example, non-exhaustive) bits referred to by a ``condition`` or - the classical variables involved in control-flow operations. - - Args: - operation: the :class:`~.circuit.Operation` instance for a node. - - Returns: - Iterable: the additional wires inherent to this operation. - """ - # If updating this, you likely need to update `_may_have_additional_wires` too. - if (condition := getattr(operation, "condition", None)) is not None: - if isinstance(condition, expr.Expr): - yield from _wires_from_expr(condition) - else: - yield from condition_resources(condition).clbits - if isinstance(operation, ControlFlowOp): - yield from operation.iter_captured_vars() - if isinstance(operation, SwitchCaseOp): - target = operation.target - if isinstance(target, Clbit): - yield target - elif isinstance(target, ClassicalRegister): - yield from target - else: - yield from _wires_from_expr(target) - elif isinstance(operation, Store): - yield from _wires_from_expr(operation.lvalue) - yield from _wires_from_expr(operation.rvalue) - - -def _wires_from_expr(node: expr.Expr) -> Iterable[Clbit | expr.Var]: - for var in expr.iter_vars(node): - if isinstance(var.var, Clbit): - yield var.var - elif isinstance(var.var, ClassicalRegister): - yield from var.var - else: - yield var +from qiskit._accelerate.circuit import DAGCircuit # pylint: disable=unused-import diff --git a/qiskit/dagcircuit/dagnode.py b/qiskit/dagcircuit/dagnode.py index 9f35f6eda898..60e2a7465707 100644 --- a/qiskit/dagcircuit/dagnode.py +++ b/qiskit/dagcircuit/dagnode.py @@ -21,7 +21,6 @@ from qiskit.circuit import ( Clbit, ClassicalRegister, - ControlFlowOp, IfElseOp, WhileLoopOp, SwitchCaseOp, @@ -175,67 +174,3 @@ def _for_loop_eq(node1, node2, bit_indices1, bit_indices2): } _SEMANTIC_EQ_SYMMETRIC = frozenset({"barrier", "swap", "break_loop", "continue_loop"}) - - -# Note: called from dag_node.rs. -def _semantic_eq(node1, node2, bit_indices1, bit_indices2): - """ - Check if DAG nodes are considered equivalent, e.g., as a node_match for - :func:`rustworkx.is_isomorphic_node_match`. - - Args: - node1 (DAGOpNode, DAGInNode, DAGOutNode): A node to compare. - node2 (DAGOpNode, DAGInNode, DAGOutNode): The other node to compare. - bit_indices1 (dict): Dictionary mapping Bit instances to their index - within the circuit containing node1 - bit_indices2 (dict): Dictionary mapping Bit instances to their index - within the circuit containing node2 - - Return: - Bool: If node1 == node2 - """ - if not isinstance(node1, DAGOpNode) or not isinstance(node1, DAGOpNode): - return type(node1) is type(node2) and bit_indices1.get(node1.wire) == bit_indices2.get( - node2.wire - ) - if isinstance(node1.op, ControlFlowOp) and isinstance(node2.op, ControlFlowOp): - # While control-flow operations aren't represented natively in the DAG, we have to do - # some unpleasant dispatching and very manual handling. Once they have more first-class - # support we'll still be dispatching, but it'll look more appropriate (like the dispatch - # based on `DAGOpNode`/`DAGInNode`/`DAGOutNode` that already exists) and less like we're - # duplicating code from the `ControlFlowOp` classes. - if type(node1.op) is not type(node2.op): - return False - comparer = _SEMANTIC_EQ_CONTROL_FLOW.get(type(node1.op)) - if comparer is None: # pragma: no cover - raise RuntimeError(f"unhandled control-flow operation: {type(node1.op)}") - return comparer(node1, node2, bit_indices1, bit_indices2) - - node1_qargs = [bit_indices1[qarg] for qarg in node1.qargs] - node1_cargs = [bit_indices1[carg] for carg in node1.cargs] - - node2_qargs = [bit_indices2[qarg] for qarg in node2.qargs] - node2_cargs = [bit_indices2[carg] for carg in node2.cargs] - - # For barriers, qarg order is not significant so compare as sets - if node1.op.name == node2.op.name and node1.name in _SEMANTIC_EQ_SYMMETRIC: - node1_qargs = set(node1_qargs) - node1_cargs = set(node1_cargs) - node2_qargs = set(node2_qargs) - node2_cargs = set(node2_cargs) - - return ( - node1_qargs == node2_qargs - and node1_cargs == node2_cargs - and _legacy_condition_eq( - getattr(node1.op, "condition", None), - getattr(node2.op, "condition", None), - bit_indices1, - bit_indices2, - ) - and node1.op == node2.op - ) - - -# Bind semantic_eq from Python to Rust implementation -DAGNode.semantic_eq = staticmethod(_semantic_eq) diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 86c5cba8295f..633de3a64f78 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -639,7 +639,8 @@ def __call__( """ if use_dag: - from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode + from qiskit.dagcircuit.dagcircuit import DAGCircuit + from qiskit.dagcircuit.dagnode import DAGOpNode sequence = self._inner_decomposer( np.asarray(unitary, dtype=complex), @@ -659,7 +660,7 @@ def __call__( op = CircuitInstruction.from_standard( gate, qubits=tuple(q[x] for x in qubits), params=params ) - node = DAGOpNode.from_instruction(op, dag=dag) + node = DAGOpNode.from_instruction(op) dag._apply_op_node_back(node) return dag else: diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index a5a3936b1d7e..5737385af15b 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -313,10 +313,7 @@ def _replace_node(self, dag, node, instr_map): if node.params: parameter_map = dict(zip(target_params, node.params)) for inner_node in target_dag.topological_op_nodes(): - new_node = DAGOpNode.from_instruction( - inner_node._to_circuit_instruction(), - dag=target_dag, - ) + new_node = DAGOpNode.from_instruction(inner_node._to_circuit_instruction()) new_node.qargs = tuple( node.qargs[target_dag.find_bit(x).index] for x in inner_node.qargs ) @@ -366,7 +363,6 @@ def _replace_node(self, dag, node, instr_map): for inner_node in target_dag.topological_op_nodes(): new_node = DAGOpNode.from_instruction( inner_node._to_circuit_instruction(), - dag=target_dag, ) new_node.qargs = tuple( node.qargs[target_dag.find_bit(x).index] for x in inner_node.qargs diff --git a/qiskit/transpiler/passes/basis/unroll_3q_or_more.py b/qiskit/transpiler/passes/basis/unroll_3q_or_more.py index 73e1d4ac5484..0c6d780f052a 100644 --- a/qiskit/transpiler/passes/basis/unroll_3q_or_more.py +++ b/qiskit/transpiler/passes/basis/unroll_3q_or_more.py @@ -58,7 +58,9 @@ def run(self, dag): continue if isinstance(node.op, ControlFlowOp): - node.op = control_flow.map_blocks(self.run, node.op) + dag.substitute_node( + node, control_flow.map_blocks(self.run, node.op), propagate_condition=False + ) continue if self.target is not None: diff --git a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py index 99bf95147ae2..51e116033bb6 100644 --- a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py +++ b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py @@ -66,7 +66,9 @@ def run(self, dag): for node in dag.op_nodes(): if isinstance(node.op, ControlFlowOp): - node.op = control_flow.map_blocks(self.run, node.op) + dag.substitute_node( + node, control_flow.map_blocks(self.run, node.op), propagate_condition=False + ) continue if getattr(node.op, "_directive", False): diff --git a/qiskit/transpiler/passes/calibration/rzx_templates.py b/qiskit/transpiler/passes/calibration/rzx_templates.py index 406e5e75de04..10f4d19ebd9e 100644 --- a/qiskit/transpiler/passes/calibration/rzx_templates.py +++ b/qiskit/transpiler/passes/calibration/rzx_templates.py @@ -20,17 +20,6 @@ from qiskit.circuit.library.templates import rzx -class RZXTemplateMap(Enum): - """Mapping of instruction name to decomposition template.""" - - ZZ1 = rzx.rzx_zz1() - ZZ2 = rzx.rzx_zz2() - ZZ3 = rzx.rzx_zz3() - YZ = rzx.rzx_yz() - XZ = rzx.rzx_xz() - CY = rzx.rzx_cy() - - def rzx_templates(template_list: List[str] = None) -> Dict: """Convenience function to get the cost_dict and templates for template matching. @@ -40,6 +29,17 @@ def rzx_templates(template_list: List[str] = None) -> Dict: Returns: Decomposition templates and cost values. """ + + class RZXTemplateMap(Enum): + """Mapping of instruction name to decomposition template.""" + + ZZ1 = rzx.rzx_zz1() + ZZ2 = rzx.rzx_zz2() + ZZ3 = rzx.rzx_zz3() + YZ = rzx.rzx_yz() + XZ = rzx.rzx_xz() + CY = rzx.rzx_cy() + if template_list is None: template_list = ["zz1", "zz2", "zz3", "yz", "xz", "cy"] diff --git a/qiskit/transpiler/passes/layout/apply_layout.py b/qiskit/transpiler/passes/layout/apply_layout.py index 629dc32061fc..7514ac6b421f 100644 --- a/qiskit/transpiler/passes/layout/apply_layout.py +++ b/qiskit/transpiler/passes/layout/apply_layout.py @@ -118,6 +118,6 @@ def run(self, dag): } out_layout = Layout(final_layout_mapping) self.property_set["final_layout"] = out_layout - new_dag._global_phase = dag._global_phase + new_dag.global_phase = dag.global_phase return new_dag diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 4ce94ecdb62f..7e7031ec3361 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -310,7 +310,7 @@ def run(self, dag): mapped_dag.add_captured_var(var) for var in dag.iter_declared_vars(): mapped_dag.add_declared_var(var) - mapped_dag._global_phase = dag._global_phase + mapped_dag.global_phase = dag.global_phase self.property_set["original_qubit_indices"] = { bit: index for index, bit in enumerate(dag.qubits) } diff --git a/qiskit/transpiler/passes/optimization/commutative_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_cancellation.py index adfc4d73a221..836fa112fd84 100644 --- a/qiskit/transpiler/passes/optimization/commutative_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_cancellation.py @@ -219,5 +219,7 @@ def _handle_control_flow_ops(self, dag): for block in node.op.blocks: new_circ = pass_manager.run(block) mapped_blocks.append(new_circ) - node.op = node.op.replace_blocks(mapped_blocks) + dag.substitute_node( + node, node.op.replace_blocks(mapped_blocks), propagate_condition=False + ) return dag diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index 12f7285af9dc..49f227e8a746 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -202,7 +202,11 @@ def _handle_control_flow_ops(self, dag): for node in dag.op_nodes(): if node.name not in CONTROL_FLOW_OP_NAMES: continue - node.op = node.op.replace_blocks(pass_manager.run(block) for block in node.op.blocks) + dag.substitute_node( + node, + node.op.replace_blocks(pass_manager.run(block) for block in node.op.blocks), + propagate_condition=False, + ) return dag def _check_not_in_basis(self, dag, gate_name, qargs): diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index e7c502c9ef9f..181f02e312b3 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -242,10 +242,8 @@ def run(self, dag): qubit = run[0].qargs for gate, angles in best_circuit_sequence: op = CircuitInstruction.from_standard(gate, qubit, angles) - node = DAGOpNode.from_instruction(op, dag=dag) - node._node_id = dag._multi_graph.add_node(node) - dag._increment_op(gate.name) - dag._multi_graph.insert_node_on_in_edges(node._node_id, first_node_id) + node = DAGOpNode.from_instruction(op) + dag._insert_1q_on_incoming_qubit(node, first_node_id) dag.global_phase += best_circuit_sequence.global_phase # Delete the other nodes in the run for current_node in run: diff --git a/qiskit/transpiler/passes/optimization/optimize_annotated.py b/qiskit/transpiler/passes/optimization/optimize_annotated.py index fe6fe7f49e78..0a583259c800 100644 --- a/qiskit/transpiler/passes/optimization/optimize_annotated.py +++ b/qiskit/transpiler/passes/optimization/optimize_annotated.py @@ -125,7 +125,9 @@ def _run_inner(self, dag) -> Tuple[DAGCircuit, bool]: # Handle control-flow for node in dag.op_nodes(): if isinstance(node.op, ControlFlowOp): - node.op = control_flow.map_blocks(self.run, node.op) + dag.substitute_node( + node, control_flow.map_blocks(self.run, node.op), propagate_condition=False + ) # First, optimize every node in the DAG. dag, opt1 = self._canonicalize(dag) @@ -163,7 +165,7 @@ def _canonicalize(self, dag) -> Tuple[DAGCircuit, bool]: node.op.modifiers = canonical_modifiers else: # no need for annotated operations - node.op = cur + dag.substitute_node(node, cur, propagate_condition=False) did_something = True return dag, did_something diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index 7508c9440a6e..ac04043a27fa 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -14,7 +14,8 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.circuit.quantumcircuitdata import CircuitInstruction -from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagnode import DAGOpNode from qiskit.circuit.library.generalized_gates import UnitaryGate from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitWeylDecomposition @@ -60,12 +61,12 @@ def run(self, dag: DAGCircuit): ur = decomp.K1r ur_node = DAGOpNode.from_instruction( - CircuitInstruction(UnitaryGate(ur), qubits=(node.qargs[0],)), dag=new_dag + CircuitInstruction(UnitaryGate(ur), qubits=(node.qargs[0],)) ) ul = decomp.K1l ul_node = DAGOpNode.from_instruction( - CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)), dag=new_dag + CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)) ) new_dag._apply_op_node_back(ur_node) new_dag._apply_op_node_back(ul_node) diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 788f0d995754..9edd1ceee445 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -372,7 +372,7 @@ def empty_dag(block): empty.add_clbits(block.clbits) for creg in block.cregs: empty.add_creg(creg) - empty._global_phase = block.global_phase + empty.global_phase = block.global_phase return empty def apply_swaps(dest_dag, swaps, layout): @@ -388,7 +388,7 @@ def recurse(dest_dag, source_dag, result, root_logical_map, layout): the virtual qubit in the root source DAG that it is bound to.""" swap_map, node_order, node_block_results = result for node_id in node_order: - node = source_dag._multi_graph[node_id] + node = source_dag.node(node_id) if node_id in swap_map: apply_swaps(dest_dag, swap_map[node_id], layout) if not node.is_control_flow(): diff --git a/qiskit/transpiler/passes/routing/stochastic_swap.py b/qiskit/transpiler/passes/routing/stochastic_swap.py index a3ebbd6cbdde..efbd7e37f626 100644 --- a/qiskit/transpiler/passes/routing/stochastic_swap.py +++ b/qiskit/transpiler/passes/routing/stochastic_swap.py @@ -223,7 +223,7 @@ def _layer_permutation(self, dag, layer_partition, layout, qubit_subset, couplin int_layout = nlayout.NLayout(layout_mapping, num_qubits, coupling.size()) trial_circuit = DAGCircuit() # SWAP circuit for slice of swaps in this trial - trial_circuit.add_qubits(layout.get_virtual_bits()) + trial_circuit.add_qubits(list(layout.get_virtual_bits())) edges = np.asarray(coupling.get_edges(), dtype=np.uint32).ravel() cdist = coupling._dist_matrix @@ -273,9 +273,7 @@ def _layer_update(self, dag, layer, best_layout, best_depth, best_circuit): # Output any swaps if best_depth > 0: logger.debug("layer_update: there are swaps in this layer, depth %d", best_depth) - dag.compose( - best_circuit, qubits={bit: bit for bit in best_circuit.qubits}, inline_captures=True - ) + dag.compose(best_circuit, qubits=list(best_circuit.qubits), inline_captures=True) else: logger.debug("layer_update: there are no swaps in this layer") # Output this layer diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 0e351161f7df..2217d32f847c 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -370,7 +370,10 @@ def _pad( op = prev_node.op theta_l, phi_l, lam_l = op.params op.params = Optimize1qGates.compose_u3(theta, phi, lam, theta_l, phi_l, lam_l) - prev_node.op = op + new_prev_node = dag.substitute_node(prev_node, op, propagate_condition=False) + start_time = self.property_set["node_start_time"].pop(prev_node) + if start_time is not None: + self.property_set["node_start_time"][new_prev_node] = start_time sequence_gphase += phase else: # Don't do anything if there's no single-qubit gate to absorb the inverse diff --git a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py index 2c7c97ec856a..d9f3d77f2915 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -72,9 +72,9 @@ def _get_node_duration( # Note that node duration is updated (but this is analysis pass) op = node.op.to_mutable() op.duration = duration - node.op = op + dag.substitute_node(node, op, propagate_condition=False) else: - duration = node.op.duration + duration = node.duration if isinstance(duration, ParameterExpression): raise TranspilerError( diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index 08ac932d8aeb..f4f70210b785 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -108,7 +108,7 @@ def run(self, dag: DAGCircuit): op = node.op.to_mutable() op.duration = duration op.unit = time_unit - node.op = op + dag.substitute_node(node, op, propagate_condition=False) self.property_set["time_unit"] = time_unit return dag diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 38c46547b12d..73a402dc202f 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -303,8 +303,10 @@ def _run(self, dag: DAGCircuit, tracker: QubitTracker) -> DAGCircuit: # next check control flow elif node.is_control_flow(): - node.op = control_flow.map_blocks( - partial(self._run, tracker=tracker.copy()), node.op + dag.substitute_node( + node, + control_flow.map_blocks(partial(self._run, tracker=tracker.copy()), node.op), + propagate_condition=False, ) # now we are free to synthesize diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 4054a158d12a..e31f6918f452 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -52,7 +52,8 @@ RGate, ) from qiskit.converters import circuit_to_dag, dag_to_circuit -from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagnode import DAGOpNode from qiskit.exceptions import QiskitError from qiskit.providers.models.backendproperties import BackendProperties from qiskit.quantum_info import Operator @@ -511,7 +512,7 @@ def _run_main_loop( for node in dag.op_nodes(): if node.name not in CONTROL_FLOW_OP_NAMES: continue - node.op = node.op.replace_blocks( + new_op = node.op.replace_blocks( [ dag_to_circuit( self._run_main_loop( @@ -530,6 +531,7 @@ def _run_main_loop( for block in node.op.blocks ] ) + dag.substitute_node(node, new_op, propagate_condition=False) out_dag = dag.copy_empty_like() for node in dag.topological_op_nodes(): @@ -572,15 +574,13 @@ def _run_main_loop( user_gate_node._to_circuit_instruction().replace( params=user_gate_node.params, qubits=tuple(qubits[x] for x in qargs), - ), - dag=out_dag, + ) ) else: node = DAGOpNode.from_instruction( CircuitInstruction.from_standard( gate, tuple(qubits[x] for x in qargs), params - ), - dag=out_dag, + ) ) out_dag._apply_op_node_back(node) out_dag.global_phase += global_phase diff --git a/qiskit/transpiler/passes/utils/control_flow.py b/qiskit/transpiler/passes/utils/control_flow.py index 3739852b4c7e..27c3c83d53c7 100644 --- a/qiskit/transpiler/passes/utils/control_flow.py +++ b/qiskit/transpiler/passes/utils/control_flow.py @@ -45,7 +45,7 @@ def trivial_recurse(method): use :func:`map_blocks` as:: if isinstance(node.op, ControlFlowOp): - node.op = map_blocks(self.run, node.op) + dag.substitute_node(node, map_blocks(self.run, node.op)) from with :meth:`.BasePass.run`.""" @@ -55,7 +55,9 @@ def bound_wrapped_method(dag): return out(self, dag) for node in dag.op_nodes(ControlFlowOp): - node.op = map_blocks(bound_wrapped_method, node.op) + dag.substitute_node( + node, map_blocks(bound_wrapped_method, node.op), propagate_condition=False + ) return method(self, dag) return out diff --git a/qiskit/transpiler/passes/utils/gate_direction.py b/qiskit/transpiler/passes/utils/gate_direction.py index 79493ae8ad25..8ea77f7ccd46 100644 --- a/qiskit/transpiler/passes/utils/gate_direction.py +++ b/qiskit/transpiler/passes/utils/gate_direction.py @@ -175,7 +175,7 @@ def _run_coupling_map(self, dag, wire_map, edges=None): # Don't include directives to avoid things like barrier, which are assumed always supported. for node in dag.op_nodes(include_directives=False): if isinstance(node.op, ControlFlowOp): - node.op = node.op.replace_blocks( + new_op = node.op.replace_blocks( dag_to_circuit( self._run_coupling_map( circuit_to_dag(block), @@ -188,6 +188,7 @@ def _run_coupling_map(self, dag, wire_map, edges=None): ) for block in node.op.blocks ) + dag.substitute_node(node, new_op, propagate_condition=False) continue if len(node.qargs) != 2: continue @@ -222,7 +223,7 @@ def _run_target(self, dag, wire_map): # Don't include directives to avoid things like barrier, which are assumed always supported. for node in dag.op_nodes(include_directives=False): if isinstance(node.op, ControlFlowOp): - node.op = node.op.replace_blocks( + new_op = node.op.replace_blocks( dag_to_circuit( self._run_target( circuit_to_dag(block), @@ -234,6 +235,7 @@ def _run_target(self, dag, wire_map): ) for block in node.op.blocks ) + dag.substitute_node(node, new_op, propagate_condition=False) continue if len(node.qargs) != 2: continue diff --git a/qiskit/transpiler/passes/utils/merge_adjacent_barriers.py b/qiskit/transpiler/passes/utils/merge_adjacent_barriers.py index 40390a02cade..acb748bb6f95 100644 --- a/qiskit/transpiler/passes/utils/merge_adjacent_barriers.py +++ b/qiskit/transpiler/passes/utils/merge_adjacent_barriers.py @@ -108,7 +108,7 @@ def _collect_potential_merges(dag, barriers): for next_barrier in barriers[1:]: # Ensure barriers are adjacent before checking if they are mergeable. - if dag._multi_graph.has_edge(end_of_barrier._node_id, next_barrier._node_id): + if dag._has_edge(end_of_barrier._node_id, next_barrier._node_id): # Remove all barriers that have already been included in this new barrier from the # set of ancestors/descendants as they will be removed from the new DAG when it is diff --git a/qiskit/visualization/circuit/_utils.py b/qiskit/visualization/circuit/_utils.py index e6ee03905d27..d933d38b5c4a 100644 --- a/qiskit/visualization/circuit/_utils.py +++ b/qiskit/visualization/circuit/_utils.py @@ -454,14 +454,12 @@ def _get_layered_instructions( clbits = new_clbits dag = circuit_to_dag(circuit) - dag.qubits = qubits - dag.clbits = clbits if justify == "none": for node in dag.topological_op_nodes(): nodes.append([node]) else: - nodes = _LayerSpooler(dag, justify, measure_map) + nodes = _LayerSpooler(dag, qubits, clbits, justify, measure_map) # Optionally remove all idle wires and instructions that are on them and # on them only. @@ -515,23 +513,25 @@ def _get_gate_span(qubits, node): def _any_crossover(qubits, node, nodes): """Return True .IFF. 'node' crosses over any 'nodes'.""" - gate_span = _get_gate_span(qubits, node) - all_indices = [] - for check_node in nodes: - if check_node != node: - all_indices += _get_gate_span(qubits, check_node) - return any(i in gate_span for i in all_indices) + return bool( + set(_get_gate_span(qubits, node)).intersection( + bit for check_node in nodes for bit in _get_gate_span(qubits, check_node) + ) + ) + + +_GLOBAL_NID = 0 class _LayerSpooler(list): """Manipulate list of layer dicts for _get_layered_instructions.""" - def __init__(self, dag, justification, measure_map): + def __init__(self, dag, qubits, clbits, justification, measure_map): """Create spool""" super().__init__() self.dag = dag - self.qubits = dag.qubits - self.clbits = dag.clbits + self.qubits = qubits + self.clbits = clbits self.justification = justification self.measure_map = measure_map self.cregs = [self.dag.cregs[reg] for reg in self.dag.cregs] @@ -660,6 +660,15 @@ def slide_from_right(self, node, index): def add(self, node, index): """Add 'node' where it belongs, starting the try at 'index'.""" + # Before we add the node, we set its node ID to be globally unique + # within this spooler. This is necessary because nodes may span + # layers (which are separate DAGs), and thus can falsely compare + # as equal if their contents and node IDs happen to be the same. + # This is particularly important for the matplotlib drawer, which + # keys several of its internal data structures with these nodes. + global _GLOBAL_NID # pylint: disable=global-statement + node._node_id = _GLOBAL_NID + _GLOBAL_NID += 1 if self.justification == "left": self.slide_from_left(node, index) else: diff --git a/qiskit/visualization/dag_visualization.py b/qiskit/visualization/dag_visualization.py index dae97b51b0a4..a6a111fe75e9 100644 --- a/qiskit/visualization/dag_visualization.py +++ b/qiskit/visualization/dag_visualization.py @@ -15,9 +15,14 @@ """ Visualization function for DAG circuit representation. """ + +import io +import subprocess + from rustworkx.visualization import graphviz_draw from qiskit.dagcircuit.dagnode import DAGOpNode, DAGInNode, DAGOutNode +from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.circuit import Qubit, Clbit, ClassicalRegister from qiskit.circuit.classical import expr from qiskit.converters import dagdependency_to_circuit @@ -26,7 +31,48 @@ from .exceptions import VisualizationError +IMAGE_TYPES = { + "canon", + "cmap", + "cmapx", + "cmapx_np", + "dia", + "dot", + "fig", + "gd", + "gd2", + "gif", + "hpgl", + "imap", + "imap_np", + "ismap", + "jpe", + "jpeg", + "jpg", + "mif", + "mp", + "pcl", + "pdf", + "pic", + "plain", + "plain-ext", + "png", + "ps", + "ps2", + "svg", + "svgz", + "vml", + "vmlz", + "vrml", + "vtx", + "wbmp", + "xdor", + "xlib", +} + + @_optionals.HAS_GRAPHVIZ.require_in_call +@_optionals.HAS_PIL.require_in_call def dag_drawer(dag, scale=0.7, filename=None, style="color"): """Plot the directed acyclic graph (dag) to represent operation dependencies in a quantum circuit. @@ -48,6 +94,8 @@ def dag_drawer(dag, scale=0.7, filename=None, style="color"): Raises: VisualizationError: when style is not recognized. InvalidFileError: when filename provided is not valid + ValueError: If the file extension for ``filename`` is not an image + type supported by Graphviz. Example: .. plot:: @@ -69,6 +117,7 @@ def dag_drawer(dag, scale=0.7, filename=None, style="color"): dag = circuit_to_dag(circ) dag_drawer(dag) """ + from PIL import Image # NOTE: use type str checking to avoid potential cyclical import # the two tradeoffs ere that it will not handle subclasses and it is @@ -215,16 +264,53 @@ def edge_attr_func(edge): e["label"] = label return e - image_type = None + image_type = "png" if filename: if "." not in filename: raise InvalidFileError("Parameter 'filename' must be in format 'name.extension'") image_type = filename.split(".")[-1] - return graphviz_draw( - dag._multi_graph, - node_attr_func, - edge_attr_func, - graph_attrs, - filename, - image_type, - ) + if image_type not in IMAGE_TYPES: + raise ValueError( + "The specified value for the image_type argument, " + f"'{image_type}' is not a valid choice. It must be one of: " + f"{IMAGE_TYPES}" + ) + + if isinstance(dag, DAGCircuit): + dot_str = dag._to_dot( + graph_attrs, + node_attr_func, + edge_attr_func, + ) + + prog = "dot" + if not filename: + dot_result = subprocess.run( + [prog, "-T", image_type], + input=dot_str.encode("utf-8"), + capture_output=True, + encoding=None, + check=True, + text=False, + ) + dot_bytes_image = io.BytesIO(dot_result.stdout) + image = Image.open(dot_bytes_image) + return image + else: + subprocess.run( + [prog, "-T", image_type, "-o", filename], + input=dot_str, + check=True, + encoding="utf8", + text=True, + ) + return None + else: + return graphviz_draw( + dag._multi_graph, + node_attr_func, + edge_attr_func, + graph_attrs, + filename, + image_type, + ) diff --git a/releasenotes/notes/dag-oxide-60b3d7219cb21703.yaml b/releasenotes/notes/dag-oxide-60b3d7219cb21703.yaml new file mode 100644 index 000000000000..090a8226c111 --- /dev/null +++ b/releasenotes/notes/dag-oxide-60b3d7219cb21703.yaml @@ -0,0 +1,49 @@ +--- +features_transpiler: + - | + The implementation of the :class:`.DAGCircuit` has been rewritten in Rust. This rewrite of + the Python class should be fully API compatible with the previous Python implementation of + the class. While the class was previously implemented using + `rustworkx `__ and its underlying data graph structure existed + in Rust, the implementation of the class and all the data was stored in Python. This new + version of :class:`.DAGCircuit` stores a Rust native representation for all its data and + is fully implemented in Rust. This new implementation should be more efficient in memory + usage as it compresses the qubit and clbit representation for instructions at rest. + It also enables speed up for transpiler passes as they can fully manipulate a + :class:`.DAGCircuit` from Rust. +upgrade_transpiler: + - | + :class:`.DAGNode` objects (and its subclasses :class:`.DAGInNode`, :class:`.DAGOutNode`, and + :class:`.DAGOpNode`) no longer return references to the same underlying object from + :class:`.DAGCircuit` methods. This was never a guarantee before that all returned nodes would + be shared reference to the same object, but with the migration of the :class:`.DAGCircuit` to + Rust when a :class:`.DAGNode` a new :class:`.DAGNode` instance is generated on the fly when + a node is returned to Python. These objects will evaluate as equal using ``==`` or similar + checks that rely on ``__eq__`` but will no longer identify as the same object. + - | + The :class:`.DAGOpNode` instances returned from the :class:`.DAGCircuit` are no longer shared + references to the underlying data stored on the DAG. In previous release it was possible to + do something like:: + + for node in dag.op_nodes(): + node.op = new_op + + however this type of mutation was always unsound as it could break the DAG's internal caching + and cause corruption of the data structure. Instead you should use the API provided by + :class:`.DAGCircuit` for mutation such as :meth:`.DAGCircuit.substitute_node`, + :meth:`.DAGCircuit.substitute_node_with_dag`, or :meth:`.DAGCircuit.contract_node`. For example + the above code block would become:: + + for node in dag.op_nodes(): + dag.substitute_node(node, new_op) + + This is similar to an upgrade note from 1.2.0 where this was noted on for mutation of the + :attr:`.DAGOpNode.op` attribute, not the :class:`.DAGOpNode` itself. However in 1.3 this extends + to the entire object, not just it's inner ``op`` attribute. In general this type of mutation was + always unsound and not supported, but could previously have potentially worked in some cases. +fixes: + - | + Fixed an issue with :meth:`.DAGCircuit.apply_operation_back` and + :meth:`.DAGCircuit.apply_operation_front` where previously if you set a + :class:`.Clbit` object to the input for the ``qargs`` argument it would silently be accepted. + This has been fixed so the type mismatch is correctly identified and an exception is raised. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index c3a930aed1f2..edb3df63e46d 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -3320,19 +3320,19 @@ def _visit_block(circuit, qubit_mapping=None): tqc_dag = circuit_to_dag(tqc) qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} input_node = tqc_dag.input_map[tqc_dag.clbits[0]] - first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node = tqc_dag._find_successors_by_edge( input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] # The first node should be a measurement self.assertIsInstance(first_meas_node.op, Measure) # This should be in the first component self.assertIn(qubit_map[first_meas_node.qargs[0]], components[0]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while isinstance(op_node, DAGOpNode): self.assertIn(qubit_map[op_node.qargs[0]], components[1]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] @@ -3394,19 +3394,19 @@ def _visit_block(circuit, qubit_mapping=None): tqc_dag = circuit_to_dag(tqc) qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} input_node = tqc_dag.input_map[tqc_dag.clbits[0]] - first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node = tqc_dag._find_successors_by_edge( input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] # The first node should be a measurement self.assertIsInstance(first_meas_node.op, Measure) # This should be in the first component self.assertIn(qubit_map[first_meas_node.qargs[0]], components[0]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while isinstance(op_node, DAGOpNode): self.assertIn(qubit_map[op_node.qargs[0]], components[1]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] @@ -3477,26 +3477,26 @@ def _visit_block(circuit, qubit_mapping=None): tqc_dag = circuit_to_dag(tqc) qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} input_node = tqc_dag.input_map[tqc_dag.clbits[0]] - first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node = tqc_dag._find_successors_by_edge( input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] self.assertIsInstance(first_meas_node.op, Measure) self.assertIn(qubit_map[first_meas_node.qargs[0]], components[0]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while not isinstance(op_node.op, Measure): self.assertIn(qubit_map[op_node.qargs[0]], components[1]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] self.assertIn(qubit_map[op_node.qargs[0]], components[1]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while not isinstance(op_node.op, Measure): self.assertIn(qubit_map[op_node.qargs[0]], components[2]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] self.assertIn(qubit_map[op_node.qargs[0]], components[2]) diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 26f7e4788083..ef8050961066 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -18,7 +18,6 @@ import unittest from ddt import ddt, data -import rustworkx as rx from numpy import pi from qiskit.dagcircuit import DAGCircuit, DAGOpNode, DAGInNode, DAGOutNode, DAGCircuitError @@ -55,18 +54,16 @@ def raise_if_dagcircuit_invalid(dag): DAGCircuitError: if DAGCircuit._multi_graph is inconsistent. """ - multi_graph = dag._multi_graph - - if not rx.is_directed_acyclic_graph(multi_graph): + if not dag._is_dag(): raise DAGCircuitError("multi_graph is not a DAG.") # Every node should be of type in, out, or op. # All input/output nodes should be present in input_map/output_map. - for node in dag._multi_graph.nodes(): + for node in dag.nodes(): if isinstance(node, DAGInNode): - assert node is dag.input_map[node.wire] + assert node == dag.input_map[node.wire] elif isinstance(node, DAGOutNode): - assert node is dag.output_map[node.wire] + assert node == dag.output_map[node.wire] elif isinstance(node, DAGOpNode): continue else: @@ -78,9 +75,7 @@ def raise_if_dagcircuit_invalid(dag): assert len(node.cargs) == node.op.num_clbits # Every edge should be labled with a known wire. - edges_outside_wires = [ - edge_data for edge_data in dag._multi_graph.edges() if edge_data not in dag.wires - ] + edges_outside_wires = [edge_data for edge_data in dag._edges() if edge_data not in dag.wires] if edges_outside_wires: raise DAGCircuitError( f"multi_graph contains one or more edges ({edges_outside_wires}) " @@ -103,7 +98,7 @@ def raise_if_dagcircuit_invalid(dag): out_node_id = dag.output_map[wire]._node_id while cur_node_id != out_node_id: - out_edges = dag._multi_graph.out_edges(cur_node_id) + out_edges = dag._out_edges(cur_node_id) edges_to_follow = [(src, dest, data) for (src, dest, data) in out_edges if data == wire] assert len(edges_to_follow) == 1 @@ -112,18 +107,15 @@ def raise_if_dagcircuit_invalid(dag): # Wires can only terminate at input/output nodes. op_counts = Counter() for op_node in dag.op_nodes(): - assert multi_graph.in_degree(op_node._node_id) == multi_graph.out_degree(op_node._node_id) + assert sum(1 for _ in dag.predecessors(op_node)) == sum(1 for _ in dag.successors(op_node)) op_counts[op_node.name] += 1 # The _op_names attribute should match the counted op names - assert op_counts == dag._op_names + assert op_counts == dag.count_ops() # Node input/output edges should match node qarg/carg/condition. for node in dag.op_nodes(): - in_edges = dag._multi_graph.in_edges(node._node_id) - out_edges = dag._multi_graph.out_edges(node._node_id) - - in_wires = {data for src, dest, data in in_edges} - out_wires = {data for src, dest, data in out_edges} + in_wires = set(dag._in_wires(node._node_id)) + out_wires = set(dag._out_wires(node._node_id)) node_cond_bits = set( node.op.condition[0][:] if getattr(node.op, "condition", None) is not None else [] @@ -516,6 +508,38 @@ def test_remove_unknown_clbit(self): self.assert_cregs_equal(self.original_cregs) self.assert_clbits_equal(self.original_clbits) + def test_remove_clbit_with_control_flow(self): + """Test clbit removal in the middle of clbits with control flow.""" + qr = QuantumRegister(1) + cr1 = ClassicalRegister(2, "a") + cr2 = ClassicalRegister(2, "b") + clbit = Clbit() + dag = DAGCircuit() + dag.add_qreg(qr) + dag.add_creg(cr1) + dag.add_creg(cr2) + dag.add_clbits([clbit]) + + inner = QuantumCircuit(1) + inner.h(0) + inner.z(0) + + op = IfElseOp(expr.logic_and(expr.equal(cr1, 3), expr.logic_not(clbit)), inner, None) + dag.apply_operation_back(op, qr, ()) + dag.remove_clbits(*cr2) + self.assertEqual(dag.clbits, list(cr1) + [clbit]) + self.assertEqual(dag.cregs, {"a": cr1}) + + expected = DAGCircuit() + expected.add_qreg(qr) + expected.add_creg(cr1) + expected.add_clbits([clbit]) + + op = IfElseOp(expr.logic_and(expr.equal(cr1, 3), expr.logic_not(clbit)), inner, None) + expected.apply_operation_back(op, qr, ()) + + self.assertEqual(dag, expected) + class TestDagApplyOperation(QiskitTestCase): """Test adding an op node to a dag.""" @@ -575,7 +599,7 @@ def test_apply_operation_back_conditional(self): self.assertEqual(h_node.op.condition, h_gate.condition) self.assertEqual( - sorted(self.dag._multi_graph.in_edges(h_node._node_id)), + sorted(self.dag._in_edges(h_node._node_id)), sorted( [ (self.dag.input_map[self.qubit2]._node_id, h_node._node_id, self.qubit2), @@ -586,7 +610,7 @@ def test_apply_operation_back_conditional(self): ) self.assertEqual( - sorted(self.dag._multi_graph.out_edges(h_node._node_id)), + sorted(self.dag._out_edges(h_node._node_id)), sorted( [ (h_node._node_id, self.dag.output_map[self.qubit2]._node_id, self.qubit2), @@ -596,7 +620,7 @@ def test_apply_operation_back_conditional(self): ), ) - self.assertTrue(rx.is_directed_acyclic_graph(self.dag._multi_graph)) + self.assertTrue(self.dag._is_dag()) def test_apply_operation_back_conditional_measure(self): """Test consistency of apply_operation_back for conditional measure.""" @@ -615,7 +639,7 @@ def test_apply_operation_back_conditional_measure(self): self.assertEqual(meas_node.op.condition, meas_gate.condition) self.assertEqual( - sorted(self.dag._multi_graph.in_edges(meas_node._node_id)), + sorted(self.dag._in_edges(meas_node._node_id)), sorted( [ (self.dag.input_map[self.qubit0]._node_id, meas_node._node_id, self.qubit0), @@ -630,7 +654,7 @@ def test_apply_operation_back_conditional_measure(self): ) self.assertEqual( - sorted(self.dag._multi_graph.out_edges(meas_node._node_id)), + sorted(self.dag._out_edges(meas_node._node_id)), sorted( [ (meas_node._node_id, self.dag.output_map[self.qubit0]._node_id, self.qubit0), @@ -644,7 +668,7 @@ def test_apply_operation_back_conditional_measure(self): ), ) - self.assertTrue(rx.is_directed_acyclic_graph(self.dag._multi_graph)) + self.assertTrue(self.dag._is_dag()) def test_apply_operation_back_conditional_measure_to_self(self): """Test consistency of apply_operation_back for measure onto conditioning bit.""" @@ -660,7 +684,7 @@ def test_apply_operation_back_conditional_measure_to_self(self): self.assertEqual(meas_node.op.condition, meas_gate.condition) self.assertEqual( - sorted(self.dag._multi_graph.in_edges(meas_node._node_id)), + sorted(self.dag._in_edges(meas_node._node_id)), sorted( [ (self.dag.input_map[self.qubit1]._node_id, meas_node._node_id, self.qubit1), @@ -671,7 +695,7 @@ def test_apply_operation_back_conditional_measure_to_self(self): ) self.assertEqual( - sorted(self.dag._multi_graph.out_edges(meas_node._node_id)), + sorted(self.dag._out_edges(meas_node._node_id)), sorted( [ (meas_node._node_id, self.dag.output_map[self.qubit1]._node_id, self.qubit1), @@ -681,7 +705,7 @@ def test_apply_operation_back_conditional_measure_to_self(self): ), ) - self.assertTrue(rx.is_directed_acyclic_graph(self.dag._multi_graph)) + self.assertTrue(self.dag._is_dag()) def test_apply_operation_front(self): """The apply_operation_front() method""" @@ -935,8 +959,8 @@ def test_classical_predecessors(self): self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) self.dag.apply_operation_back(HGate(), [self.qubit0], []) self.dag.apply_operation_back(HGate(), [self.qubit1], []) - self.dag.apply_operation_back(Measure(), [self.qubit0, self.clbit0], []) - self.dag.apply_operation_back(Measure(), [self.qubit1, self.clbit1], []) + self.dag.apply_operation_back(Measure(), [self.qubit0], [self.clbit0]) + self.dag.apply_operation_back(Measure(), [self.qubit1], [self.clbit1]) predecessor_measure = self.dag.classical_predecessors(self.dag.named_nodes("measure").pop()) @@ -948,6 +972,13 @@ def test_classical_predecessors(self): self.assertIsInstance(predecessor1, DAGInNode) self.assertIsInstance(predecessor1.wire, Clbit) + def test_apply_operation_reject_invalid_qarg_carg(self): + """Test that we can't add a carg to qargs and vice versa on apply methods""" + with self.assertRaises(KeyError): + self.dag.apply_operation_back(Measure(), [self.clbit1], [self.qubit1]) + with self.assertRaises(KeyError): + self.dag.apply_operation_front(Measure(), [self.clbit1], [self.qubit1]) + def test_classical_successors(self): """The method dag.classical_successors() returns successors connected by classical edges""" @@ -969,8 +1000,8 @@ def test_classical_successors(self): self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) self.dag.apply_operation_back(HGate(), [self.qubit0], []) self.dag.apply_operation_back(HGate(), [self.qubit1], []) - self.dag.apply_operation_back(Measure(), [self.qubit0, self.clbit0], []) - self.dag.apply_operation_back(Measure(), [self.qubit1, self.clbit1], []) + self.dag.apply_operation_back(Measure(), [self.qubit0], [self.clbit0]) + self.dag.apply_operation_back(Measure(), [self.qubit1], [self.clbit1]) successors_measure = self.dag.classical_successors(self.dag.named_nodes("measure").pop()) @@ -1067,12 +1098,12 @@ def test_topological_nodes(self): ("cx", (self.qubit2, self.qubit1)), ("cx", (self.qubit0, self.qubit2)), ("h", (self.qubit2,)), + cr[0], + cr[1], qr[0], qr[1], qr[2], cr[0], - cr[0], - cr[1], cr[1], ] self.assertEqual( @@ -2397,7 +2428,6 @@ def test_contract_var_use_to_nothing(self): expected = DAGCircuit() expected.add_input_var(a) - self.assertEqual(src, expected) def test_raise_if_var_mismatch(self): @@ -2665,7 +2695,6 @@ def test_substituting_node_preserves_args_condition(self, inplace): self.assertEqual(replacement_node.qargs, (qr[1], qr[0])) self.assertEqual(replacement_node.cargs, ()) self.assertEqual(replacement_node.op.condition, (cr, 1)) - self.assertEqual(replacement_node is node_to_be_replaced, inplace) @data(True, False) @@ -3172,24 +3201,34 @@ def test_creg_conditional(self): self.assertEqual(gate_node.qargs, (self.qreg[0],)) self.assertEqual(gate_node.cargs, ()) self.assertEqual(gate_node.op.condition, (self.creg, 1)) + + gate_node_preds = list(self.dag.predecessors(gate_node)) + gate_node_in_edges = [ + (src._node_id, wire) + for (src, tgt, wire) in self.dag.edges(gate_node_preds) + if tgt == gate_node + ] + self.assertEqual( - sorted(self.dag._multi_graph.in_edges(gate_node._node_id)), + sorted(gate_node_in_edges), sorted( [ - (self.dag.input_map[self.qreg[0]]._node_id, gate_node._node_id, self.qreg[0]), - (self.dag.input_map[self.creg[0]]._node_id, gate_node._node_id, self.creg[0]), - (self.dag.input_map[self.creg[1]]._node_id, gate_node._node_id, self.creg[1]), + (self.dag.input_map[self.qreg[0]]._node_id, self.qreg[0]), + (self.dag.input_map[self.creg[0]]._node_id, self.creg[0]), + (self.dag.input_map[self.creg[1]]._node_id, self.creg[1]), ] ), ) + gate_node_out_edges = [(tgt._node_id, wire) for (_, tgt, wire) in self.dag.edges(gate_node)] + self.assertEqual( - sorted(self.dag._multi_graph.out_edges(gate_node._node_id)), + sorted(gate_node_out_edges), sorted( [ - (gate_node._node_id, self.dag.output_map[self.qreg[0]]._node_id, self.qreg[0]), - (gate_node._node_id, self.dag.output_map[self.creg[0]]._node_id, self.creg[0]), - (gate_node._node_id, self.dag.output_map[self.creg[1]]._node_id, self.creg[1]), + (self.dag.output_map[self.qreg[0]]._node_id, self.qreg[0]), + (self.dag.output_map[self.creg[0]]._node_id, self.creg[0]), + (self.dag.output_map[self.creg[1]]._node_id, self.creg[1]), ] ), ) @@ -3204,22 +3243,31 @@ def test_clbit_conditional(self): self.assertEqual(gate_node.qargs, (self.qreg[0],)) self.assertEqual(gate_node.cargs, ()) self.assertEqual(gate_node.op.condition, (self.creg[0], 1)) + + gate_node_preds = list(self.dag.predecessors(gate_node)) + gate_node_in_edges = [ + (src._node_id, wire) + for (src, tgt, wire) in self.dag.edges(gate_node_preds) + if tgt == gate_node + ] + self.assertEqual( - sorted(self.dag._multi_graph.in_edges(gate_node._node_id)), + sorted(gate_node_in_edges), sorted( [ - (self.dag.input_map[self.qreg[0]]._node_id, gate_node._node_id, self.qreg[0]), - (self.dag.input_map[self.creg[0]]._node_id, gate_node._node_id, self.creg[0]), + (self.dag.input_map[self.qreg[0]]._node_id, self.qreg[0]), + (self.dag.input_map[self.creg[0]]._node_id, self.creg[0]), ] ), ) + gate_node_out_edges = [(tgt._node_id, wire) for (_, tgt, wire) in self.dag.edges(gate_node)] self.assertEqual( - sorted(self.dag._multi_graph.out_edges(gate_node._node_id)), + sorted(gate_node_out_edges), sorted( [ - (gate_node._node_id, self.dag.output_map[self.qreg[0]]._node_id, self.qreg[0]), - (gate_node._node_id, self.dag.output_map[self.creg[0]]._node_id, self.creg[0]), + (self.dag.output_map[self.qreg[0]]._node_id, self.qreg[0]), + (self.dag.output_map[self.creg[0]]._node_id, self.creg[0]), ] ), ) diff --git a/test/python/transpiler/_dummy_passes.py b/test/python/transpiler/_dummy_passes.py index 64956e541024..eaddfe6d1ddd 100644 --- a/test/python/transpiler/_dummy_passes.py +++ b/test/python/transpiler/_dummy_passes.py @@ -122,10 +122,10 @@ class PassF_reduce_dag_property(DummyTP): def run(self, dag): super().run(dag) - if not hasattr(dag, "property"): - dag.property = 8 - dag.property = round(dag.property * 0.8) - logging.getLogger(logger).info("dag property = %i", dag.property) + if dag.duration is None: + dag.duration = 8 + dag.duration = round(dag.duration * 0.8) + logging.getLogger(logger).info("dag property = %i", dag.duration) return dag @@ -138,8 +138,8 @@ class PassG_calculates_dag_property(DummyAP): def run(self, dag): super().run(dag) - if hasattr(dag, "property"): - self.property_set["property"] = dag.property + if dag.duration is not None: + self.property_set["property"] = dag.duration else: self.property_set["property"] = 8 logging.getLogger(logger).info( diff --git a/test/python/transpiler/test_collect_multiq_blocks.py b/test/python/transpiler/test_collect_multiq_blocks.py index 58589d838e33..e2446d03da2a 100644 --- a/test/python/transpiler/test_collect_multiq_blocks.py +++ b/test/python/transpiler/test_collect_multiq_blocks.py @@ -93,6 +93,7 @@ def test_block_interrupted_by_gate(self): # but equivalent between python 3.5 and 3.7 # there is no implied topology in a block, so this isn't an issue dag_nodes = [set(dag_nodes[:4]), set(dag_nodes[4:])] + pass_nodes = [set(bl) for bl in pass_.property_set["block_list"]] self.assertEqual(dag_nodes, pass_nodes) diff --git a/test/python/visualization/test_utils.py b/test/python/visualization/test_utils.py index 1dca43b6beb2..1ef87994d001 100644 --- a/test/python/visualization/test_utils.py +++ b/test/python/visualization/test_utils.py @@ -48,18 +48,18 @@ def test_get_layered_instructions(self): (qregs, cregs, layered_ops) = _utils._get_layered_instructions(self.circuit) exp = [ - [("cx", (self.qr2[0], self.qr2[1]), ()), ("cx", (self.qr1[0], self.qr1[1]), ())], - [("measure", (self.qr2[0],), (self.cr2[0],))], - [("measure", (self.qr1[0],), (self.cr1[0],))], - [("cx", (self.qr2[1], self.qr2[0]), ()), ("cx", (self.qr1[1], self.qr1[0]), ())], - [("measure", (self.qr2[1],), (self.cr2[1],))], - [("measure", (self.qr1[1],), (self.cr1[1],))], + {("cx", (self.qr2[0], self.qr2[1]), ()), ("cx", (self.qr1[0], self.qr1[1]), ())}, + {("measure", (self.qr2[0],), (self.cr2[0],))}, + {("measure", (self.qr1[0],), (self.cr1[0],))}, + {("cx", (self.qr2[1], self.qr2[0]), ()), ("cx", (self.qr1[1], self.qr1[0]), ())}, + {("measure", (self.qr2[1],), (self.cr2[1],))}, + {("measure", (self.qr1[1],), (self.cr1[1],))}, ] self.assertEqual([self.qr1[0], self.qr1[1], self.qr2[0], self.qr2[1]], qregs) self.assertEqual([self.cr1[0], self.cr1[1], self.cr2[0], self.cr2[1]], cregs) self.assertEqual( - exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_reverse_bits(self): @@ -69,18 +69,18 @@ def test_get_layered_instructions_reverse_bits(self): ) exp = [ - [("cx", (self.qr2[0], self.qr2[1]), ()), ("cx", (self.qr1[0], self.qr1[1]), ())], - [("measure", (self.qr2[0],), (self.cr2[0],))], - [("measure", (self.qr1[0],), (self.cr1[0],)), ("cx", (self.qr2[1], self.qr2[0]), ())], - [("cx", (self.qr1[1], self.qr1[0]), ())], - [("measure", (self.qr2[1],), (self.cr2[1],))], - [("measure", (self.qr1[1],), (self.cr1[1],))], + {("cx", (self.qr2[0], self.qr2[1]), ()), ("cx", (self.qr1[0], self.qr1[1]), ())}, + {("measure", (self.qr2[0],), (self.cr2[0],))}, + {("measure", (self.qr1[0],), (self.cr1[0],)), ("cx", (self.qr2[1], self.qr2[0]), ())}, + {("cx", (self.qr1[1], self.qr1[0]), ())}, + {("measure", (self.qr2[1],), (self.cr2[1],))}, + {("measure", (self.qr1[1],), (self.cr1[1],))}, ] self.assertEqual([self.qr2[1], self.qr2[0], self.qr1[1], self.qr1[0]], qregs) self.assertEqual([self.cr2[1], self.cr2[0], self.cr1[1], self.cr1[0]], cregs) self.assertEqual( - exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_remove_idle_wires(self): @@ -103,18 +103,18 @@ def test_get_layered_instructions_remove_idle_wires(self): (qregs, cregs, layered_ops) = _utils._get_layered_instructions(circuit, idle_wires=False) exp = [ - [("cx", (qr2[0], qr2[1]), ()), ("cx", (qr1[0], qr1[1]), ())], - [("measure", (qr2[0],), (cr2[0],))], - [("measure", (qr1[0],), (cr1[0],))], - [("cx", (qr2[1], qr2[0]), ()), ("cx", (qr1[1], qr1[0]), ())], - [("measure", (qr2[1],), (cr2[1],))], - [("measure", (qr1[1],), (cr1[1],))], + {("cx", (qr2[0], qr2[1]), ()), ("cx", (qr1[0], qr1[1]), ())}, + {("measure", (qr2[0],), (cr2[0],))}, + {("measure", (qr1[0],), (cr1[0],))}, + {("cx", (qr2[1], qr2[0]), ()), ("cx", (qr1[1], qr1[0]), ())}, + {("measure", (qr2[1],), (cr2[1],))}, + {("measure", (qr1[1],), (cr1[1],))}, ] self.assertEqual([qr1[0], qr1[1], qr2[0], qr2[1]], qregs) self.assertEqual([cr1[0], cr1[1], cr2[0], cr2[1]], cregs) self.assertEqual( - exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_left_justification_simple(self): @@ -136,15 +136,15 @@ def test_get_layered_instructions_left_justification_simple(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc, justify="left") l_exp = [ - [ + { ("h", (Qubit(QuantumRegister(4, "q"), 1),), ()), ("h", (Qubit(QuantumRegister(4, "q"), 2),), ()), - ], - [("cx", (Qubit(QuantumRegister(4, "q"), 0), Qubit(QuantumRegister(4, "q"), 3)), ())], + }, + {("cx", (Qubit(QuantumRegister(4, "q"), 0), Qubit(QuantumRegister(4, "q"), 3)), ())}, ] self.assertEqual( - l_exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + l_exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_right_justification_simple(self): @@ -166,15 +166,15 @@ def test_get_layered_instructions_right_justification_simple(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc, justify="right") r_exp = [ - [("cx", (Qubit(QuantumRegister(4, "q"), 0), Qubit(QuantumRegister(4, "q"), 3)), ())], - [ + {("cx", (Qubit(QuantumRegister(4, "q"), 0), Qubit(QuantumRegister(4, "q"), 3)), ())}, + { ("h", (Qubit(QuantumRegister(4, "q"), 1),), ()), ("h", (Qubit(QuantumRegister(4, "q"), 2),), ()), - ], + }, ] self.assertEqual( - r_exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + r_exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_left_justification_less_simple(self): @@ -215,33 +215,33 @@ def test_get_layered_instructions_left_justification_less_simple(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc, justify="left") l_exp = [ - [ + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())], - [ + }, + {("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())}, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [("u2", (Qubit(QuantumRegister(5, "q"), 1),), ())], - [ + }, + {("u2", (Qubit(QuantumRegister(5, "q"), 1),), ())}, + { ( "measure", (Qubit(QuantumRegister(5, "q"), 0),), (Clbit(ClassicalRegister(1, "c1"), 0),), ) - ], - [("u2", (Qubit(QuantumRegister(5, "q"), 0),), ())], - [("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())], - [ + }, + {("u2", (Qubit(QuantumRegister(5, "q"), 0),), ())}, + {("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())}, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], + }, ] self.assertEqual( - l_exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + l_exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_right_justification_less_simple(self): @@ -282,35 +282,35 @@ def test_get_layered_instructions_right_justification_less_simple(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc, justify="right") r_exp = [ - [ + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())], - [ + }, + {("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())}, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [ + }, + { ( "measure", (Qubit(QuantumRegister(5, "q"), 0),), (Clbit(ClassicalRegister(1, "c1"), 0),), ) - ], - [ + }, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())], - [ + }, + {("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())}, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], + }, ] self.assertEqual( - r_exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + r_exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_op_with_cargs(self): @@ -335,25 +335,25 @@ def test_get_layered_instructions_op_with_cargs(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc) expected = [ - [("h", (Qubit(QuantumRegister(2, "q"), 0),), ())], - [ + {("h", (Qubit(QuantumRegister(2, "q"), 0),), ())}, + { ( "measure", (Qubit(QuantumRegister(2, "q"), 0),), (Clbit(ClassicalRegister(2, "c"), 0),), ) - ], - [ + }, + { ( "add_circ", (Qubit(QuantumRegister(2, "q"), 1),), (Clbit(ClassicalRegister(2, "c"), 0),), ) - ], + }, ] self.assertEqual( - expected, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + expected, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) @unittest.skipUnless(optionals.HAS_PYLATEX, "needs pylatexenc")