From e663f577698a8b117f9252f8cd6ac83d171f79b8 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 27 Jun 2024 14:10:56 +0300 Subject: [PATCH 1/3] 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 18ad1aa3a0910787dccaa7cf1e227166cc7d0797 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Mon, 8 Jul 2024 13:04:08 +0300 Subject: [PATCH 2/3] 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 3/3] 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> {