Repository: PennyLaneAI/pennylane (python)

Issue:
Display `id` of gates when printing with `qml.draw`
With the new functionality to assign "names" to gates, it might be worthwhile to have this information propagate to the printed circuit of `qml.draw`. I'm thinking something like
```
def circuit(x,d):
    qml.RX(d, id="data", wires=0)
    qml.Rot(x, id="parameters", wires=0)
   return qml.expval(qml.PauliZ(0)) 
   
d = 0.123
x = np.array([0.863, 0.611, 0.281])
print(qml.draw(circuit)(x,d))
0: ──RX(0.123, "data")──Rot(0.863, 0.611, 0.281, "parameters")──┤ ⟨Z⟩
```

We could add an keyword argument to `qml.draw` to expose or suppress this info during printing

Pull request description:
Add operator IDs into the text based circuit draw function.
**Context:**
Per the associated issue "With the new functionality to assign "names" to gates, it might be worthwhile to have this information propagate to the printed circuit of qml.draw."

**Description of the Change:**
Modifies the printed text output resulting from qml.draw() to indicate the ids of operators.

```python
def circuit(x,d):
    qml.RX(d, id="data", wires=0)
    qml.Rot(x, wires=0)
    return qml.expval(qml.PauliZ(0)) 
   
d = 0.123
x = np.array([0.863, 0.611, 0.281])
print(qml.draw(circuit)(*x,d))
```
Output:
```0: ──RX(0.123,"data")──Rot(0.863,0.611,0.281)──┤ ⟨Z⟩```

**Benefits:**
This allows users to see operators displayed with their ids in the printed circuit output.

**Possible Drawbacks:**
Too many rendered operators with ID's might consume more space causing the printed circuit to wrap more.

**Related GitHub Issues:**
This implements and closes #1397


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 73865ec7b..d73eada4a 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -12,6 +12,9 @@
 * `qml.ArbitraryUnitary` now supports batching.
   [(#4745)](https://github.com/PennyLaneAI/pennylane/pull/4745)
 
+* `qml.draw` and `qml.draw_mpl` now render operator ids.
+  [(#4749)](https://github.com/PennyLaneAI/pennylane/pull/4749)
+
 <h3>Breaking changes 💔</h3>
 
 <h3>Deprecations 👋</h3>
@@ -26,3 +29,4 @@ This release contains contributions from (in alphabetical order):
 
 Amintor Dusko,
 David Wierichs,
+Justin Woodring
diff --git a/pennylane/drawer/draw.py b/pennylane/drawer/draw.py
index 5b3432233..b43e53f4b 100644
--- a/pennylane/drawer/draw.py
+++ b/pennylane/drawer/draw.py
@@ -76,13 +76,13 @@ def draw(
         def circuit(a, w):
             qml.Hadamard(0)
             qml.CRX(a, wires=[0, 1])
-            qml.Rot(*w, wires=[1])
+            qml.Rot(*w, wires=[1], id="arbitrary")
             qml.CRX(-a, wires=[0, 1])
             return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
 
     >>> print(qml.draw(circuit)(a=2.3, w=[1.2, 3.2, 0.7]))
-    0: ──H─╭●─────────────────────────────╭●─────────┤ ╭<Z@Z>
-    1: ────╰RX(2.30)──Rot(1.20,3.20,0.70)─╰RX(-2.30)─┤ ╰<Z@Z>
+    0: ──H─╭●─────────────────────────────────────────╭●─────────┤ ╭<Z@Z>
+    1: ────╰RX(2.30)──Rot(1.20,3.20,0.70,"arbitrary")─╰RX(-2.30)─┤ ╰<Z@Z>
 
     .. details::
         :title: Usage Details
@@ -91,14 +91,14 @@ def draw(
     By specifying the ``decimals`` keyword, parameters are displayed to the specified precision.
 
     >>> print(qml.draw(circuit, decimals=4)(a=2.3, w=[1.2, 3.2, 0.7]))
-    0: ──H─╭●─────────────────────────────────────╭●───────────┤ ╭<Z@Z>
-    1: ────╰RX(2.3000)──Rot(1.2000,3.2000,0.7000)─╰RX(-2.3000)─┤ ╰<Z@Z>
+    0: ──H─╭●─────────────────────────────────────────────────╭●───────────┤ ╭<Z@Z>
+    1: ────╰RX(2.3000)──Rot(1.2000,3.2000,0.7000,"arbitrary")─╰RX(-2.3000)─┤ ╰<Z@Z>
 
     Parameters can be omitted by requesting ``decimals=None``:
 
     >>> print(qml.draw(circuit, decimals=None)(a=2.3, w=[1.2, 3.2, 0.7]))
-    0: ──H─╭●───────╭●──┤ ╭<Z@Z>
-    1: ────╰RX──Rot─╰RX─┤ ╰<Z@Z>
+    0: ──H─╭●────────────────────╭●──┤ ╭<Z@Z>
+    1: ────╰RX──Rot("arbitrary")─╰RX─┤ ╰<Z@Z>
 
     If the parameters are not acted upon by classical processing like ``-a``, then
     ``qml.draw`` can handle string-valued parameters as well:
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 619fabbb6..bb4dcf7c2 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -927,12 +927,17 @@ class Operator(abc.ABC):
         >>> op = qml.RX(1.23456, wires=0)
         >>> op.label()
         "RX"
-        >>> op.label(decimals=2)
-        "RX\n(1.23)"
         >>> op.label(base_label="my_label")
         "my_label"
+        >>> op = qml.RX(1.23456, wires=0, id="test_data")
+        >>> op.label()
+        "RX("test_data")"
+        >>> op.label(decimals=2)
+        "RX\n(1.23,"test_data")"
+        >>> op.label(base_label="my_label")
+        "my_label("test_data")"
         >>> op.label(decimals=2, base_label="my_label")
-        "my_label\n(1.23)"
+        "my_label\n(1.23,"test_data")"
 
         If the operation has a matrix-valued parameter and a cache dictionary is provided,
         unique matrices will be cached in the ``'matrices'`` key list. The label will contain
@@ -960,7 +965,7 @@ class Operator(abc.ABC):
         op_label = base_label or self.__class__.__name__
 
         if self.num_params == 0:
-            return op_label
+            return op_label if self._id is None else f'{op_label}("{self._id}")'
 
         params = self.parameters
 
@@ -974,21 +979,29 @@ class Operator(abc.ABC):
                 or not isinstance(cache.get("matrices", None), list)
                 or len(params) != 1
             ):
-                return op_label
+                return op_label if self._id is None else f'{op_label}("{self._id}")'
 
             for i, mat in enumerate(cache["matrices"]):
                 if qml.math.shape(params[0]) == qml.math.shape(mat) and qml.math.allclose(
                     params[0], mat
                 ):
-                    return f"{op_label}(M{i})"
+                    return (
+                        f"{op_label}(M{i})"
+                        if self._id is None
+                        else f'{op_label}(M{i},"{self._id}")'
+                    )
 
             # matrix not in cache
             mat_num = len(cache["matrices"])
             cache["matrices"].append(params[0])
-            return f"{op_label}(M{mat_num})"
+            return (
+                f"{op_label}(M{mat_num})"
+                if self._id is None
+                else f'{op_label}(M{mat_num},"{self._id}")'
+            )
 
         if decimals is None:
-            return op_label
+            return op_label if self._id is None else f'{op_label}("{self._id}")'
 
         def _format(x):
             try:
@@ -998,7 +1011,12 @@ class Operator(abc.ABC):
                 return format(x)
 
         param_string = ",\n".join(_format(p) for p in params)
-        return f"{op_label}\n({param_string})"
+
+        return (
+            f"{op_label}\n({param_string})"
+            if self._id is None
+            else f'{op_label}\n({param_string},"{self._id}")'
+        )
 
     def __init__(self, *params, wires=None, id=None):
         # pylint: disable=too-many-branches


Test patch:
diff --git a/tests/test_operation.py b/tests/test_operation.py
index 249182fdf..48e3e21d2 100644
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -1244,6 +1244,16 @@ class TestOperatorIntegration:
         with pytest.raises(TypeError, match="unsupported operand type"):
             _ = qml.PauliX(0) @ "dummy"
 
+    def test_label_for_operations_with_id(self):
+        """Test that the label is correctly generated for an operation with an id"""
+        op = qml.RX(1.344, wires=0, id="test_with_id")
+        assert '"test_with_id"' in op.label()
+        assert '"test_with_id"' in op.label(decimals=2)
+
+        op = qml.RX(1.344, wires=0)
+        assert '"test_with_id"' not in op.label()
+        assert '"test_with_id"' not in op.label(decimals=2)
+
 
 class TestTensor:
     """Unit tests for the Tensor class"""