diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 2ad0b5d3315d..fc7dce3826c7 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -91,6 +91,12 @@ impl PackedInstruction { .as_ref() .and_then(|args| args.condition.as_ref()) } + + 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 cc2a483d59c1..6d575d213a1f 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -17,6 +17,7 @@ use crate::circuit_instruction::{ OperationTypeConstruct, }; use crate::dag_node::{DAGInNode, DAGNode, DAGOpNode, DAGOutNode}; +use crate::dot_utils::build_dot; use crate::error::DAGCircuitError; use crate::imports::{ CLASSICAL_REGISTER, CLBIT, CONTROL_FLOW_OP, DAG_NODE, EXPR, ITER_VARS, STORE_OP, @@ -53,6 +54,7 @@ use rustworkx_core::traversal::{ }; use smallvec::SmallVec; use std::borrow::Borrow; +use std::collections::{BTreeMap, VecDeque}; use std::convert::Infallible; use std::f64::consts::PI; use std::ffi::c_double; @@ -103,7 +105,7 @@ where } #[derive(Clone, Debug)] -enum NodeType { +pub(crate) enum NodeType { QubitIn(Qubit), QubitOut(Qubit), ClbitIn(Clbit), @@ -122,7 +124,7 @@ impl NodeType { } #[derive(Clone, Debug)] -enum Wire { +pub(crate) enum Wire { Qubit(Qubit), Clbit(Clbit), Var(PyObject), @@ -213,7 +215,7 @@ pub struct DAGCircuit { metadata: Option>, calibrations: HashMap>, - dag: StableDiGraph, + pub(crate) dag: StableDiGraph, #[pyo3(get)] qregs: Py, @@ -225,9 +227,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. @@ -2636,9 +2638,9 @@ 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: @@ -2647,19 +2649,47 @@ def _format(operand): /// 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_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(), + }, + )) + } + } + + Ok(PyTuple::new_bound(py, edges) + .into_any() + .iter() + .unwrap() + .unbind()) } /// Get the list of "op" nodes in the dag. @@ -2813,14 +2843,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() @@ -2828,6 +2856,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> { @@ -2887,23 +2930,6 @@ def _format(operand): .unbind()) } - /// 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> { @@ -3084,61 +3110,85 @@ 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. - 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. @@ -3243,42 +3293,79 @@ 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 self + .qargs_cache + .intern(pred_packed.qubits_id) + .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. @@ -3315,15 +3402,108 @@ 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>( + 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)?)) } } 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())) + } + + /// 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) => { @@ -3348,6 +3528,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.target()), + _ => None, + }) + .unique() + } + /// Apply a [PackedInstruction] to the back of the circuit. /// /// The provided `instr` MUST be valid for this DAG, e.g. its @@ -3782,7 +3982,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..e04889fca0b5 --- /dev/null +++ b/crates/circuit/src/dot_utils.rs @@ -0,0 +1,108 @@ +// 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(), + }; + 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