Repository: Qiskit/qiskit

Issue:
Add `.draw()` method to `quantum_info.Operator` class
### What should we add?

It would be very convenient to have a `draw` method for the `Operator` class similar to what is available for the `Statevector` and `DensityMatrix` classes. A good start would be to just add text representations (`repr`, `text`, `latex_source` and `latex`), but more could be added later if needed, like a visual representation of Pauli decompositions, cityscape plot of the op matrix, etc. (I believe the state visualizations work for operators as well, so some of these will be minimal work too).

Having this feature will also allow the user to set a predefine way of displaying things locally in the user config file. Not sure if this would have to be a separate PR since it will require its own testing, release notes, etc.

I can implement this if there is any interest. I would just need some guidance on what would be the best way to organize the code. In the case of the `Statevector` class, the `draw` method calls the `state_drawer` function which lives under `visualization.state_visualization`. Not sure if for `Operator` something similar would have to be done, or if everything could just be defined under the `draw` method itself since a `__repr__` method already exists, and `latex_source` and `latex` only need to call the `array_to_latex` function. 

The only representation remaining would be `text`, which ideally could be implemented just by reusing the `visualization.state_visualization.TextMatrix` class, but currently this does not support operators. [This line](https://github.com/Qiskit/qiskit-terra/blob/02502b5d98796dcc2c8b80ac5d0f2af85e978e5c/qiskit/visualization/state_visualization.py#L1406) errors out because the `Operator` class does not have a `.dims` attribute, so either that would need to be modified, or a `__str__` method could be defined for `Operator` and simply use that.


Discussion:
Context: Diego brought this to me and Abby in the recent office hours, and we generally agreed it sounds like a good idea.

Resolving patch:
diff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py
--- a/qiskit/quantum_info/operators/operator.py
+++ b/qiskit/quantum_info/operators/operator.py
@@ -153,6 +153,70 @@ def settings(self):
             "output_dims": self.output_dims(),
         }
 
+    def draw(self, output=None, **drawer_args):
+        """Return a visualization of the Operator.
+
+        **repr**: String of the state's ``__repr__``.
+
+        **text**: ASCII TextMatrix that can be printed in the console.
+
+        **latex**: An IPython Latex object for displaying in Jupyter Notebooks.
+
+        **latex_source**: Raw, uncompiled ASCII source to generate array using LaTeX.
+
+        Args:
+            output (str): Select the output method to use for drawing the
+                state. Valid choices are `repr`, `text`, `latex`, `latex_source`,
+                Default is `repr`.
+            drawer_args: Arguments to be passed directly to the relevant drawing
+                function or constructor (`TextMatrix()`, `array_to_latex()`).
+                See the relevant function under `qiskit.visualization` for that function's
+                documentation.
+
+        Returns:
+            :class:`str` or :class:`TextMatrix` or :class:`IPython.display.Latex`:
+            Drawing of the Operator.
+
+        Raises:
+            ValueError: when an invalid output method is selected.
+
+        """
+        # pylint: disable=cyclic-import
+        from qiskit.visualization import array_to_latex
+
+        default_output = "repr"
+        if output is None:
+            output = default_output
+
+        if output == "repr":
+            return self.__repr__()
+
+        elif output == "text":
+            from qiskit.visualization.state_visualization import TextMatrix
+
+            return TextMatrix(self, **drawer_args)
+
+        elif output == "latex":
+            return array_to_latex(self, **drawer_args)
+
+        elif output == "latex_source":
+            return array_to_latex(self, source=True, **drawer_args)
+
+        else:
+            raise ValueError(
+                f"""'{output}' is not a valid option for drawing {type(self).__name__} objects. 
+            Please choose from: 'text', 'latex', or 'latex_source'."""
+            )
+
+    def _ipython_display_(self):
+        out = self.draw()
+        if isinstance(out, str):
+            print(out)
+        else:
+            from IPython.display import display
+
+            display(out)
+
     @classmethod
     def from_label(cls, label: str) -> Operator:
         """Return a tensor product of single-qubit operators.
diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py
--- a/qiskit/visualization/state_visualization.py
+++ b/qiskit/visualization/state_visualization.py
@@ -24,6 +24,7 @@
 import numpy as np
 from qiskit import user_config
 from qiskit.quantum_info.states.statevector import Statevector
+from qiskit.quantum_info.operators.operator import Operator
 from qiskit.quantum_info.operators.symplectic import PauliList, SparsePauliOp
 from qiskit.quantum_info.states.densitymatrix import DensityMatrix
 from qiskit.utils.deprecation import deprecate_arg, deprecate_func
@@ -1407,7 +1408,11 @@ def __init__(self, state, max_size=8, dims=None, prefix="", suffix=""):
         self.state = state
         self.max_size = max_size
         if dims is None:  # show dims if state is not only qubits
-            if set(state.dims()) == {2}:
+            if (isinstance(state, (Statevector, DensityMatrix)) and set(state.dims()) == {2}) or (
+                isinstance(state, Operator)
+                and len(state.input_dims()) == len(state.output_dims())
+                and set(state.input_dims()) == set(state.output_dims()) == {2}
+            ):
                 dims = False
             else:
                 dims = True
@@ -1432,7 +1437,12 @@ def __str__(self):
         if self.dims:
             data += ",\n"
             dimstr += " " * len(self.prefix)
-            dimstr += f"dims={self.state._op_shape.dims_l()}"
+            if isinstance(self.state, (Statevector, DensityMatrix)):
+                dimstr += f"dims={self.state._op_shape.dims_l()}"
+            else:
+                dimstr += f"input_dims={self.state.input_dims()}, "
+                dimstr += f"output_dims={self.state.output_dims()}"
+
         return self.prefix + data + dimstr + self.suffix
 
     def __repr__(self):


Test patch:
diff --git a/test/python/quantum_info/operators/test_operator.py b/test/python/quantum_info/operators/test_operator.py
--- a/test/python/quantum_info/operators/test_operator.py
+++ b/test/python/quantum_info/operators/test_operator.py
@@ -681,6 +681,18 @@ def test_reverse_qargs(self):
         state2 = Operator(circ2)
         self.assertEqual(state1.reverse_qargs(), state2)
 
+    def test_drawings(self):
+        """Test draw method"""
+        qc1 = QFT(5)
+        op = Operator.from_circuit(qc1)
+        with self.subTest(msg="str(operator)"):
+            str(op)
+        for drawtype in ["repr", "text", "latex_source"]:
+            with self.subTest(msg=f"draw('{drawtype}')"):
+                op.draw(drawtype)
+        with self.subTest(msg=" draw('latex')"):
+            op.draw("latex")
+
     def test_from_circuit_constructor_no_layout(self):
         """Test initialization from a circuit using the from_circuit constructor."""
         # Test tensor product of 1-qubit gates