Repository: Qiskit/qiskit

Issue:
Avoid pydot for graphviz based visualization
### What is the expected enhancement?

The current releases of pydot has a compatibility issue with it's upstream dependency pyparsing causing parsing errors that introduce empty nodes into visualizations (see: https://github.com/pydot/pydot/issues/280). For example: https://qiskit.org/documentation/stable/0.34/stubs/qiskit.visualization.dag_drawer.html#qiskit.visualization.dag_drawer This raises a question why do we need to be using pydot for graphviz based visualizations at all? We're just using it as a library to call `subprocess` on graphviz for us and not leveraging the parsing or dot file building capabilities of pydot (with the exception of the pass manager drawer but this is easy to refactor). 

Instead of using pydot in qiskit we have two alternative options available we either can replace the pydot usage with a direct subprocess call to graphviz, or we can leverage retworkx's built in graphviz visualization function: https://qiskit.org/documentation/retworkx/apiref/retworkx.visualization.graphviz_draw.html#retworkx.visualization.graphviz_draw
If we use the retworkx function we can remove a lot of boilerplate code to deal with i/o and image objects too.


Discussion:
HI @mtreinish can you please assign this to me. Interested in working on this enhancement.
I've assigned you, @AshwinKul28, let us know if you need anything more that's not in the [contributing guide](https://github.com/Qiskit/qiskit-terra/blob/main/CONTRIBUTING.md).
The `dag_drawer()` function has been updated in #8162 to close this issue we still need to update the coupling map drawer, pass manager drawer, and the equivalence library drawer.
The other drawers identified by @mtreinish specifically are:

- https://github.com/Qiskit/qiskit-terra/blob/f47cd45e29c8f4e30040e8039bc28465bd1bd1ef/qiskit/transpiler/coupling.py#L410
- https://github.com/Qiskit/qiskit-terra/blob/f47cd45e29c8f4e30040e8039bc28465bd1bd1ef/qiskit/visualization/pass_manager_visualization.py#L31
- https://github.com/Qiskit/qiskit-terra/blob/f47cd45e29c8f4e30040e8039bc28465bd1bd1ef/qiskit/circuit/equivalence.py#L139

I'll try to work on these too to close this issue entirely; however, it may take me some time.

Resolving patch:
diff --git a/qiskit/circuit/equivalence.py b/qiskit/circuit/equivalence.py
--- a/qiskit/circuit/equivalence.py
+++ b/qiskit/circuit/equivalence.py
@@ -12,16 +12,15 @@
 
 """Gate equivalence library."""
 
-import io
 from collections import namedtuple
 
+from retworkx.visualization import graphviz_draw
 import retworkx as rx
 
-from qiskit.exceptions import MissingOptionalLibraryError
+from qiskit.exceptions import InvalidFileError
 from .exceptions import CircuitError
 from .parameterexpression import ParameterExpression
 
-
 Key = namedtuple("Key", ["name", "num_qubits"])
 
 Entry = namedtuple("Entry", ["search_base", "equivalences"])
@@ -148,56 +147,20 @@ def draw(self, filename=None):
                 IPython SVG if in a jupyter notebook, or as a PIL.Image otherwise.
 
         Raises:
-            MissingOptionalLibraryError: when pydot or pillow are not installed.
+            InvalidFileError: if filename is not valid.
         """
-        try:
-            import pydot
-
-            has_pydot = True
-        except ImportError:
-            has_pydot = False
-        try:
-            from PIL import Image
-
-            has_pil = True
-        except ImportError:
-            has_pil = False
-
-        if not has_pydot:
-            raise MissingOptionalLibraryError(
-                libname="pydot",
-                name="EquivalenceLibrary.draw",
-                pip_install="pip install pydot",
-            )
-        if not has_pil and not filename:
-            raise MissingOptionalLibraryError(
-                libname="pillow",
-                name="EquivalenceLibrary.draw",
-                pip_install="pip install pillow",
-            )
-
-        try:
-            from IPython.display import SVG
-
-            has_ipython = True
-        except ImportError:
-            has_ipython = False
-
-        dot_str = self._build_basis_graph().to_dot(
-            lambda node: {"label": node["label"]}, lambda edge: edge
-        )
-        dot = pydot.graph_from_dot_data(dot_str)[0]
+        image_type = None
         if filename:
-            extension = filename.split(".")[-1]
-            dot.write(filename, format=extension)
-            return None
-
-        if has_ipython:
-            svg = dot.create_svg(prog="dot")
-            return SVG(svg)
-
-        png = dot.create_png(prog="dot")
-        return Image.open(io.BytesIO(png))
+            if "." not in filename:
+                raise InvalidFileError("Parameter 'filename' must be in format 'name.extension'")
+            image_type = filename.split(".")[-1]
+        return graphviz_draw(
+            self._build_basis_graph(),
+            lambda node: {"label": node["label"]},
+            lambda edge: edge,
+            filename=filename,
+            image_type=image_type,
+        )
 
     def _build_basis_graph(self):
         graph = rx.PyDiGraph()
diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py
--- a/qiskit/transpiler/coupling.py
+++ b/qiskit/transpiler/coupling.py
@@ -19,14 +19,13 @@
 onto a device with this coupling.
 """
 
-import io
 import warnings
 
 import numpy as np
 import retworkx as rx
+from retworkx.visualization import graphviz_draw
 
 from qiskit.transpiler.exceptions import CouplingError
-from qiskit.exceptions import MissingOptionalLibraryError
 
 
 class CouplingMap:
@@ -410,36 +409,12 @@ def __str__(self):
     def draw(self):
         """Draws the coupling map.
 
-        This function needs `pydot <https://github.com/erocarrera/pydot>`_,
-        which in turn needs `Graphviz <https://www.graphviz.org/>`_ to be
-        installed. Additionally, `pillow <https://python-pillow.org/>`_ will
-        need to be installed.
+        This function calls the :func:`~retworkx.visualization.graphviz_draw` function from the
+        ``retworkx`` package to draw the :class:`CouplingMap` object.
 
         Returns:
             PIL.Image: Drawn coupling map.
 
-        Raises:
-            MissingOptionalLibraryError: when pydot or pillow are not installed.
         """
-        try:
-            import pydot
-        except ImportError as ex:
-            raise MissingOptionalLibraryError(
-                libname="pydot",
-                name="coupling map drawer",
-                pip_install="pip install pydot",
-            ) from ex
 
-        try:
-            from PIL import Image
-        except ImportError as ex:
-            raise MissingOptionalLibraryError(
-                libname="pillow",
-                name="coupling map drawer",
-                pip_install="pip install pillow",
-            ) from ex
-        dot_str = self.graph.to_dot()
-        dot = pydot.graph_from_dot_data(dot_str)[0]
-        png = dot.create_png(prog="neato")
-
-        return Image.open(io.BytesIO(png))
+        return graphviz_draw(self.graph, method="neato")


Test patch:
diff --git a/test/python/circuit/test_equivalence.py b/test/python/circuit/test_equivalence.py
--- a/test/python/circuit/test_equivalence.py
+++ b/test/python/circuit/test_equivalence.py
@@ -13,6 +13,7 @@
 
 """Test Qiskit's EquivalenceLibrary class."""
 
+import unittest
 import numpy as np
 
 from qiskit.test import QiskitTestCase
@@ -21,8 +22,10 @@
 from qiskit.circuit.library import U2Gate
 from qiskit.circuit.exceptions import CircuitError
 from qiskit.converters import circuit_to_instruction, circuit_to_gate
-
 from qiskit.circuit import EquivalenceLibrary
+from qiskit.utils import optionals
+
+from ..visualization.visualization import QiskitVisualizationTestCase, path_to_diagram_reference
 
 
 class OneQubitZeroParamGate(Gate):
@@ -461,3 +464,16 @@ def test_gate_decomposition_properties(self):
 
         self.assertEqual(len(decomps), 1)
         self.assertEqual(decomps[0], qc2)
+
+
+class TestEquivalenceLibraryVisualization(QiskitVisualizationTestCase):
+    """Test cases for EquivalenceLibrary visualization."""
+
+    @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed")
+    def test_session_equivalence_draw(self):
+        """Verify SessionEquivalenceLibrary drawing with reference image."""
+        from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel
+
+        image = sel.draw()
+        image_ref = path_to_diagram_reference("session_equivalence_library.png")
+        self.assertImagesAreEqual(image, image_ref, 0.04)
diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py
--- a/test/python/transpiler/test_coupling.py
+++ b/test/python/transpiler/test_coupling.py
@@ -12,10 +12,15 @@
 
 # pylint: disable=missing-docstring
 
+import unittest
+
 from qiskit.transpiler import CouplingMap
 from qiskit.transpiler.exceptions import CouplingError
 from qiskit.providers.fake_provider import FakeRueschlikon
 from qiskit.test import QiskitTestCase
+from qiskit.utils import optionals
+
+from ..visualization.visualization import QiskitVisualizationTestCase, path_to_diagram_reference
 
 
 class CouplingTest(QiskitTestCase):
@@ -436,3 +441,13 @@ def test_subgraph(self):
         edge_list = subgraph.get_edges()
         expected = [(0, 1), (1, 2), (2, 3)]
         self.assertEqual(expected, edge_list, f"{edge_list} does not match {expected}")
+
+
+class CouplingVisualizationTest(QiskitVisualizationTestCase):
+    @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed")
+    def test_coupling_draw(self):
+        """Test that the coupling map drawing with respect to the reference file is correct."""
+        cmap = CouplingMap([[0, 1], [1, 2], [2, 3], [2, 4], [2, 5], [2, 6]])
+        image_ref = path_to_diagram_reference("coupling_map.png")
+        image = cmap.draw()
+        self.assertImagesAreEqual(image, image_ref, diff_tolerance=0.01)
diff --git a/test/python/visualization/references/coupling_map.png b/test/python/visualization/references/coupling_map.png
new file mode 100644
Binary files /dev/null and b/test/python/visualization/references/coupling_map.png differ
diff --git a/test/python/visualization/references/session_equivalence_library.png b/test/python/visualization/references/session_equivalence_library.png
new file mode 100644
Binary files /dev/null and b/test/python/visualization/references/session_equivalence_library.png differ