From e663f577698a8b117f9252f8cd6ac83d171f79b8 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 27 Jun 2024 14:10:56 +0300 Subject: [PATCH 01/11] attempt to port quantum_causal_cone to rust --- crates/circuit/src/dag_circuit.rs | 172 +++++++++++++++++++----------- 1 file changed, 111 insertions(+), 61 deletions(-) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index d3131cdfacc6..7a869644415b 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.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 std::collections::VecDeque; use crate::bit_data::BitData; use crate::circuit_instruction::PackedInstruction; use crate::circuit_instruction::{ @@ -2730,14 +2731,12 @@ def _format(operand): /// Returns iterator of the predecessors of a node that are /// connected by a quantum edge as DAGOpNodes and DAGInNodes. - fn quantum_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(_) => Some(e.source()), - _ => None, - }); - let predecessors: PyResult> = - filtered.unique().map(|i| self.get_node(py, i)).collect(); + #[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() @@ -2745,6 +2744,21 @@ def _format(operand): .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> { @@ -2781,23 +2795,6 @@ def _format(operand): todo!() } - /// Returns iterator of the successors of a node that are - /// connected by a quantum edge as DAGOpNodes and DAGOutNodes. - fn quantum_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(_) => Some(e.target()), - _ => None, - }); - 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 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> { @@ -3137,42 +3134,75 @@ def _format(operand): /// /// Returns: /// Set[~qiskit.circuit.Qubit]: The set of qubits whose interactions affect ``qubit``. - fn quantum_causal_cone(&self, qubit: &Bound) -> PyResult> { - // # 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.") - // # Add the qubit to the causal cone. - // qubits_to_check = {qubit} - // # Add predecessors of output node to the queue. - // queue = deque(self.predecessors(output_node)) - // - // # While queue isn't empty - // while queue: - // # Pop first element. - // node_to_check = queue.popleft() - // # Check whether element is input or output node. - // if isinstance(node_to_check, DAGOpNode): - // # Keep all the qubits in the operation inside a set. - // qubit_set = set(node_to_check.qargs) - // # Check if there are any qubits in common and that the operation is not a barrier. - // if ( - // len(qubit_set.intersection(qubits_to_check)) > 0 - // and node_to_check.op.name != "barrier" - // and not getattr(node_to_check.op, "_directive") - // ): - // # If so, add all the qubits to the causal cone. - // qubits_to_check = qubits_to_check.union(qubit_set) - // # For each predecessor of the current node, filter input/output nodes, - // # also make sure it has at least one qubit in common. Then append. - // for node in self.quantum_predecessors(node_to_check): - // if ( - // isinstance(node, DAGOpNode) - // and len(qubits_to_check.intersection(set(node.qargs))) > 0 - // ): - // queue.append(node) - // return qubits_to_check - todo!() + 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_output_map.get(&output_qubit).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_id).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 !qubits_in_cone.is_disjoint(&HashSet::<&Qubit>::from_iter( + self.qargs_cache.intern(pred_packed.qubits_id).iter(), + )) { + queue.push_back(pred_index); + } + } + } + } + } + } + + let elements : Vec<_> = qubits_in_cone.iter().map(|&qubit| qubit.0.into_py(py)).collect(); + Ok(PySet::new_bound(py, &elements)?.unbind()) } /// Return a dictionary of circuit properties. @@ -3242,6 +3272,26 @@ impl DAGCircuit { } } + 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.source()), + _ => None, + }) + .unique() +} + fn topological_nodes(&self) -> PyResult> { let key = |node: NodeIndex| -> Result<(Option, Option), Infallible> { Ok(self.dag.node_weight(node).unwrap().key()) From 4b8988a9889725e49683299786e5e0d91eeb490f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Tue, 2 Jul 2024 12:03:42 +0200 Subject: [PATCH 02/11] Add naive implementation of edges method. Co-authored-by: Jake Lishman --- crates/circuit/src/dag_circuit.rs | 68 +++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index c64da1d6cac4..e524007f237d 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -1920,7 +1920,6 @@ def _format(operand): // )) } - /// Yield nodes in topological order. /// /// Args: @@ -2593,30 +2592,65 @@ def _format(operand): Ok(tup.into_any().iter().unwrap().unbind()) } - /// Iterator for edge values and source and dest node + /// Iterator for edge values with source and destination node. /// - /// This works by returning the output edges from the specified nodes. If + /// 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): + /// nodes(DAGOpNode, DAGInNode, DAGOutNode or Vec of 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) - fn edges(&self, nodes: Option>) -> Py { - // 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) - todo!() + /// 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_references().map(|(index, weight)| index).collect(), + Some(nodes) => { + if let Ok(node) = get_node_index(nodes.clone()) { + let mut out = Vec::new(); + out.push(node); + out + } else { + let mut out = Vec::new(); + for node in nodes.iter()? + { + out.push(get_node_index(node?)?); + } + out + } + } + }; + + let mut edges = Vec::new(); + for node in actual_nodes { + let in_out_nodes = self.dag.edges_directed(node, Outgoing).map(|e| { + ( + self.get_node(py, e.source()), + self.get_node(py, e.target()), + match e.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + } + ) + }); + for (source, target, wire) in in_out_nodes { + edges.push((source?, target?, wire)); + } + } + + Ok(PyTuple::new_bound(py, edges) + .into_any() + .iter() + .unwrap() + .unbind()) } /// Get the list of "op" nodes in the dag. From 18ad1aa3a0910787dccaa7cf1e227166cc7d0797 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Mon, 8 Jul 2024 13:04:08 +0300 Subject: [PATCH 03/11] applying suggestions from code review --- crates/circuit/src/dag_circuit.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 7a869644415b..7ad3a89f0ee6 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -3201,8 +3201,9 @@ def _format(operand): } } - let elements : Vec<_> = qubits_in_cone.iter().map(|&qubit| qubit.0.into_py(py)).collect(); - Ok(PySet::new_bound(py, &elements)?.unbind()) + 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. @@ -3286,7 +3287,7 @@ impl DAGCircuit { self.dag .edges_directed(node, Outgoing) .filter_map(|e| match e.weight() { - Wire::Qubit(_) => Some(e.source()), + Wire::Qubit(_) => Some(e.target()), _ => None, }) .unique() From 7fee641222a7a2b1a23b144f1e5809fec9c6915f Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Mon, 8 Jul 2024 14:00:50 +0300 Subject: [PATCH 04/11] more suggestions from code review --- crates/circuit/src/dag_circuit.rs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 7ad3a89f0ee6..98b65e44bab5 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -10,7 +10,6 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use std::collections::VecDeque; use crate::bit_data::BitData; use crate::circuit_instruction::PackedInstruction; use crate::circuit_instruction::{ @@ -44,6 +43,7 @@ use rustworkx_core::petgraph::prelude::StableDiGraph; use rustworkx_core::petgraph::stable_graph::{DefaultIx, IndexType, Neighbors, NodeIndex}; use rustworkx_core::petgraph::visit::{IntoNodeReferences, NodeCount, NodeRef}; use rustworkx_core::petgraph::Incoming; +use std::collections::VecDeque; use std::convert::Infallible; use std::f64::consts::PI; use std::ffi::c_double; @@ -3190,9 +3190,12 @@ def _format(operand): if let NodeType::Operation(pred_packed) = self.dag.node_weight(pred_index).unwrap() { - if !qubits_in_cone.is_disjoint(&HashSet::<&Qubit>::from_iter( - self.qargs_cache.intern(pred_packed.qubits_id).iter(), - )) { + if self + .qargs_cache + .intern(pred_packed.qubits_id) + .iter() + .any(|x| qubits_in_cone.contains(x)) + { queue.push_back(pred_index); } } @@ -3284,14 +3287,14 @@ impl DAGCircuit { } 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() -} + self.dag + .edges_directed(node, Outgoing) + .filter_map(|e| match e.weight() { + Wire::Qubit(_) => Some(e.target()), + _ => None, + }) + .unique() + } fn topological_nodes(&self) -> PyResult> { let key = |node: NodeIndex| -> Result<(Option, Option), Infallible> { From 58c3d8ff7359c12347a664e16c759ccf589b065c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 9 Jul 2024 16:39:09 -0400 Subject: [PATCH 05/11] Add implementation of collect_runs() and collect_1q_runs() This commit adds a rust implementation of the collect runs and collect 1q runs functions to the DAGCircuit class. It adds a native rust api that returns an iterator of node indices and also a python interface that matches the expectations from the previous python implementation. The one difference is that the return type for collect_1q_runs() will change from an `rx.NodeIndices` to a Python list. This isn't a huge deal in practice as the NodeIndices is just a custom sequence type and the python list should work identically except for explicit type checks. Although, this actually matches the Python type hint of the previous implementation. There wasn't really an alternative for this because rustworkx doesn't expose an interface to create NodeIndices, they're read only return types. --- crates/circuit/src/dag_circuit.rs | 114 +++++++++++++++++++++++------- 1 file changed, 89 insertions(+), 25 deletions(-) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index f70c90863bd6..797a74b07849 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -3045,34 +3045,59 @@ def _format(operand): /// in the circuit's basis. /// /// Nodes must have only one successor to continue the run. - fn collect_runs(&self, namelist: &Bound) -> PyResult> { - // def filter_fn(node): - // return ( - // isinstance(node, DAGOpNode) - // and node.op.name in namelist - // and getattr(node.op, "condition", None) is None - // ) - // - // group_list = rx.collect_runs(self._multi_graph, filter_fn) - // return {tuple(x) for x in group_list} - todo!() + #[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. - fn collect_1q_runs(&self) -> PyResult> { - // def filter_fn(node): - // return ( - // isinstance(node, DAGOpNode) - // and len(node.qargs) == 1 - // and len(node.cargs) == 0 - // and isinstance(node.op, Gate) - // and hasattr(node.op, "__array__") - // and getattr(node.op, "condition", None) is None - // and not node.op.is_parameterized() - // ) - // - // return rx.collect_runs(self._multi_graph, filter_fn) - todo!() + #[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. @@ -3285,6 +3310,45 @@ def _format(operand): } 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.op.matrix(&inst.params).is_some() + && 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())) + } + fn increment_op(&mut self, op: String) { match self.op_names.entry(op) { hash_map::Entry::Occupied(mut o) => { From 4aa3bf453313cc9f272bc5f993de800b2336be7f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 9 Jul 2024 17:27:53 -0400 Subject: [PATCH 06/11] Add implementation of collect_2q_runs() This commit adds a rust implementation of the collect 2q runs method to the DAGCircuit class. It adds a native rust api that returns an vec of vec of node indices and also a python interface that matches the expectations from the previous python implementation. The rustworkx-core function returns a collected vec so instead of throwing that away for an iterator this just returns the owned vec so the callers can decide how to use it. The one difference is that the return type for collect_1q_runs() will change from an `rx.NodeIndices` to a Python list. This isn't a huge deal in practice as the NodeIndices is just a custom sequence type and the python list should work identically except for explicit type checks. --- crates/circuit/src/circuit_instruction.rs | 6 ++ crates/circuit/src/dag_circuit.rs | 87 ++++++++++++++++------- 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 0f1ed0477f79..eea36766b27f 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -84,6 +84,12 @@ impl PackedInstruction { py_op, } } + + pub fn is_parameterized(&self) -> bool { + self.params + .iter() + .any(|x| matches!(x, Param::ParameterExpression(_))) + } } /// A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index d7e980ca05f6..2c0b5c5f2e8d 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -43,12 +43,12 @@ use rustworkx_core::petgraph::prelude::StableDiGraph; use rustworkx_core::petgraph::stable_graph::{DefaultIx, IndexType, Neighbors, NodeIndex}; use rustworkx_core::petgraph::visit::{IntoNodeReferences, NodeCount, NodeRef}; use rustworkx_core::petgraph::Incoming; -use std::collections::VecDeque; use rustworkx_core::traversal::{ ancestors as core_ancestors, bfs_successors as core_bfs_successors, descendants as core_descendants, }; use std::borrow::Borrow; +use std::collections::VecDeque; use std::convert::Infallible; use std::f64::consts::PI; use std::ffi::c_double; @@ -3098,30 +3098,29 @@ def _format(operand): } /// Return a set of non-conditional runs of 2q "op" nodes. - fn collect_2q_runs(&self) -> PyResult> { - // to_qid = {} - // for i, qubit in enumerate(self.qubits): - // to_qid[qubit] = i - // - // def filter_fn(node): - // if isinstance(node, DAGOpNode): - // return ( - // isinstance(node.op, Gate) - // and len(node.qargs) <= 2 - // and not getattr(node.op, "condition", None) - // and not node.op.is_parameterized() - // ) - // else: - // return None - // - // def color_fn(edge): - // if isinstance(edge, Qubit): - // return to_qid[edge] - // else: - // return None - // - // return rx.collect_bicolor_runs(self._multi_graph, filter_fn, color_fn) - todo!() + #[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. @@ -3383,6 +3382,44 @@ impl DAGCircuit { .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 { + OperationType::Standard(gate) => Ok(Some( + gate.num_qubits() <= 2 + && match &inst.extra_attrs { + None => true, + Some(attrs) => attrs.condition.is_none(), + } + && !inst.is_parameterized(), + )), + OperationType::Gate(gate) => Ok(Some( + gate.num_qubits() <= 2 + && match &inst.extra_attrs { + None => true, + Some(attrs) => attrs.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)), + Wire::Clbit(_) => Ok(None), + } + }; + rustworkx_core::dag_algo::collect_bicolor_runs(&self.dag, filter_fn, color_fn).unwrap() + } + fn increment_op(&mut self, op: String) { match self.op_names.entry(op) { hash_map::Entry::Occupied(mut o) => { From 5edd93696fa73fcc6f8866f25395812d4cff240c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 10 Jul 2024 11:55:41 +0200 Subject: [PATCH 07/11] Apply suggestions from Kevin's code review --- crates/circuit/src/dag_circuit.rs | 39 +++++++++++++------------------ 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index e524007f237d..b2cefb6067fa 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -2598,7 +2598,7 @@ def _format(operand): /// no nodes are specified all edges from the graph are returned. /// /// Args: - /// nodes(DAGOpNode, DAGInNode, DAGOutNode or Vec of DAGOpNode, DAGInNode, or DAGOutNode: + /// 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. /// @@ -2606,43 +2606,36 @@ def _format(operand): /// 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 { + let get_node_index = |obj: &Bound| -> PyResult { Ok(obj.downcast::()?.borrow().node.unwrap()) }; let actual_nodes: Vec<_> = match nodes { - None => self.dag.node_references().map(|(index, weight)| index).collect(), + None => self.dag.node_indices().collect(), Some(nodes) => { - if let Ok(node) = get_node_index(nodes.clone()) { - let mut out = Vec::new(); + let mut out = Vec::new(); + if let Ok(node) = get_node_index(&nodes) { out.push(node); - out } else { - let mut out = Vec::new(); - for node in nodes.iter()? - { - out.push(get_node_index(node?)?); + for node in nodes.iter()? { + out.push(get_node_index(&node?)?); } - out - } + } + out } }; let mut edges = Vec::new(); for node in actual_nodes { - let in_out_nodes = self.dag.edges_directed(node, Outgoing).map(|e| { - ( - self.get_node(py, e.source()), - self.get_node(py, e.target()), - match e.weight() { + 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(), - } - ) - }); - for (source, target, wire) in in_out_nodes { - edges.push((source?, target?, wire)); + }, + )) } } From 27efc47fc40fc5d955589bfb72ee8aa26683ca56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:58:17 +0200 Subject: [PATCH 08/11] Format docstring --- crates/circuit/src/dag_circuit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index b2cefb6067fa..d1d05b99c5fe 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -2598,7 +2598,7 @@ def _format(operand): /// no nodes are specified all edges from the graph are returned. /// /// Args: - /// nodes(DAGOpNode, DAGInNode, or DAGOutNode|list(DAGOpNode, DAGInNode, or DAGOutNode): + /// 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. /// From bb44505b0a95b991a337f71056e7be2e63eaec2c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 10 Jul 2024 10:13:32 -0400 Subject: [PATCH 09/11] Implement dag.draw() method This commit implements the dag.draw() method for the new rust dagcircuit struct. To accomplish this first the dot_utils.rs module from rustworkx is forked and included in qiskit circuit crate. This module is used to generate a dot file using python callbacks, it is modified to work with the dagcircuit types directly. Eventually this can be ported to rustworkx-core and made more generic, but honestly there isn't too much of a need because that will require a lot of boiler plate on the caller side here. With a method to generate a dot file from rust for a dagcircuit implement this is then exposed to Python as a new private method `._to_dot()` which will return a dot file string. The Python space dag drawer is then modified to instead of relying on rustworkx's graphviz_draw() method to instead call `dag._to_dot()` and manually pass that to graphviz to generate a visualization. This is essentially what rustworkx's graphviz_draw() is already doing under the covers. Then the final piece to implement the rust implementation of the draw() method is to use pyo3 to call the python space dag_drawer() function which is how the previous python implementation's method worked. --- crates/circuit/src/dag_circuit.rs | 41 +++++--- crates/circuit/src/dot_utils.rs | 109 ++++++++++++++++++++++ crates/circuit/src/lib.rs | 1 + qiskit/visualization/dag_visualization.py | 37 ++++++-- 4 files changed, 170 insertions(+), 18 deletions(-) create mode 100644 crates/circuit/src/dot_utils.rs diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index d7e980ca05f6..395ec83379ff 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -16,6 +16,7 @@ use crate::circuit_instruction::{ convert_py_to_operation_type, CircuitInstruction, OperationTypeConstruct, }; use crate::dag_node::{DAGInNode, DAGNode, DAGOpNode, DAGOutNode}; +use crate::dot_utils::build_dot; use crate::error::DAGCircuitError; use crate::imports::{DAG_NODE, VARIABLE_MAPPER}; use crate::interner::{Index, IndexedInterner, Interner}; @@ -43,12 +44,12 @@ use rustworkx_core::petgraph::prelude::StableDiGraph; use rustworkx_core::petgraph::stable_graph::{DefaultIx, IndexType, Neighbors, NodeIndex}; use rustworkx_core::petgraph::visit::{IntoNodeReferences, NodeCount, NodeRef}; use rustworkx_core::petgraph::Incoming; -use std::collections::VecDeque; use rustworkx_core::traversal::{ ancestors as core_ancestors, bfs_successors as core_bfs_successors, descendants as core_descendants, }; use std::borrow::Borrow; +use std::collections::{BTreeMap, VecDeque}; use std::convert::Infallible; use std::f64::consts::PI; use std::ffi::c_double; @@ -99,7 +100,7 @@ where } #[derive(Clone, Debug)] -enum NodeType { +pub(crate) enum NodeType { QubitIn(Qubit), QubitOut(Qubit), ClbitIn(Clbit), @@ -118,7 +119,7 @@ impl NodeType { } #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] -enum Wire { +pub(crate) enum Wire { Qubit(Qubit), Clbit(Clbit), } @@ -137,7 +138,7 @@ pub struct DAGCircuit { metadata: Option>, calibrations: HashMap>, - dag: StableDiGraph, + pub(crate) dag: StableDiGraph, #[pyo3(get)] qregs: Py, @@ -149,9 +150,9 @@ pub struct DAGCircuit { /// The cache used to intern instruction cargs. cargs_cache: IndexedInterner>, /// Qubits registered in the circuit. - qubits: BitData, + pub(crate) qubits: BitData, /// Clbits registered in the circuit. - clbits: BitData, + pub(crate) clbits: BitData, /// Global phase. global_phase: PyObject, /// Duration. @@ -3335,11 +3336,27 @@ def _format(operand): /// 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(&self, scale: f64, filename: Option<&str>, style: &str) -> PyResult> { - // from qiskit.visualization.dag_visualization import dag_drawer - // - // return dag_drawer(dag=self, scale=scale, filename=filename, style=style) - todo!() + fn draw<'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", (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)?)) } } @@ -3679,7 +3696,7 @@ impl DAGCircuit { Ok(clbit) } - fn get_node(&self, py: Python, node: NodeIndex) -> PyResult> { + pub(crate) fn get_node(&self, py: Python, node: NodeIndex) -> PyResult> { self.unpack_into(py, node, self.dag.node_weight(node).unwrap()) } diff --git a/crates/circuit/src/dot_utils.rs b/crates/circuit/src/dot_utils.rs new file mode 100644 index 000000000000..59e8e0600fde --- /dev/null +++ b/crates/circuit/src/dot_utils.rs @@ -0,0 +1,109 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +// 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::{ + Data, EdgeRef, GraphBase, GraphProp, 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(), + }; + 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<'a, T: ToPyObject>( + py: Python, + attrs: Option<&'a 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/lib.rs b/crates/circuit/src/lib.rs index af641d6a5adb..d03981d353da 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -20,6 +20,7 @@ pub mod parameter_table; mod bit_data; mod dag_node; +mod dot_utils; mod error; mod interner; diff --git a/qiskit/visualization/dag_visualization.py b/qiskit/visualization/dag_visualization.py index 73b9c30f6dc8..787077f30bc1 100644 --- a/qiskit/visualization/dag_visualization.py +++ b/qiskit/visualization/dag_visualization.py @@ -15,7 +15,9 @@ """ Visualization function for DAG circuit representation. """ -from rustworkx.visualization import graphviz_draw + +import io +import subprocess from qiskit.dagcircuit.dagnode import DAGOpNode, DAGInNode, DAGOutNode from qiskit.circuit import Qubit, Clbit, ClassicalRegister @@ -27,6 +29,7 @@ @_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. @@ -69,6 +72,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 @@ -213,11 +217,32 @@ def edge_attr_func(edge): 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, + + dot_str = dag._to_dot( + graph_attrs, node_attr_func, edge_attr_func, - graph_attrs, - filename, - image_type, ) + + 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 From 977ca42be9eb7c16fb085216314e60e26289be3d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 10 Jul 2024 10:28:08 -0400 Subject: [PATCH 10/11] Fix copyright header to use qiskit's format instead of rustworkx's Both are apache licensed the normal header used in the libraries is just slightly different, and Qiskit enforces the formatting in CI. So this just updates the header to conform with what's expect in Qiskit CI. --- crates/circuit/src/dot_utils.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/circuit/src/dot_utils.rs b/crates/circuit/src/dot_utils.rs index 59e8e0600fde..2d27455ef185 100644 --- a/crates/circuit/src/dot_utils.rs +++ b/crates/circuit/src/dot_utils.rs @@ -1,15 +1,15 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at +// This code is part of Qiskit. // -// http://www.apache.org/licenses/LICENSE-2.0 +// (C) Copyright IBM 2022 // -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. +// 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 From b42d0ac7275ffc007940fcac9d3ffa1ff2e2f51c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 10 Jul 2024 11:33:45 -0400 Subject: [PATCH 11/11] Fix draw() method implementation --- crates/circuit/src/dag_circuit.rs | 4 ++-- crates/circuit/src/dot_utils.rs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 925a5bee493b..d09fb5ecad04 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -3336,14 +3336,14 @@ def _format(operand): /// otherwise None. #[pyo3(signature=(scale=0.7, filename=None, style="color"))] fn draw<'py>( - &self, + 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", (scale, filename, style)) + module.call_method1("dag_drawer", (slf, scale, filename, style)) } fn _to_dot<'py>( diff --git a/crates/circuit/src/dot_utils.rs b/crates/circuit/src/dot_utils.rs index 2d27455ef185..e04889fca0b5 100644 --- a/crates/circuit/src/dot_utils.rs +++ b/crates/circuit/src/dot_utils.rs @@ -21,8 +21,7 @@ use std::io::prelude::*; use crate::dag_circuit::{DAGCircuit, Wire}; use pyo3::prelude::*; use rustworkx_core::petgraph::visit::{ - Data, EdgeRef, GraphBase, GraphProp, IntoEdgeReferences, IntoNodeReferences, NodeIndexable, - NodeRef, + EdgeRef, IntoEdgeReferences, IntoNodeReferences, NodeIndexable, NodeRef, }; static TYPE: [&str; 2] = ["graph", "digraph"];