Repository: PennyLaneAI/pennylane
Pull request #1885 (2021-11-10 15:44:55)

New text circuit drawer
This method can hopefully replace `CircuitGraph.draw()` and associated objects like `CircuitDrawer`, `RepresentationResolver` and `Charset`.

This function already:
* is much faster (about 8x)
*  has more features 
    - `max_length` Issue #1841 
    -  `decimals` for printing scalar parameters to arbitrary precision
* has fewer bugs and peculiarities 
    -naturally supports templates
    -broadcasts `qml.state` across all wires

*Downsides*: This function does not display matrix-valued parameters below the circuit diagram.   Circuit drawings are for inspecting whether or not everything is generally in the correct place.  Users can inspect specific parameters via other means.

```
with qml.tape.QuantumTape() as tape:
    qml.QFT(wires=(0,1,2))
    qml.RX(1.234, wires=0)
    qml.RY(1.234, wires=1)
    qml.RZ(1.234, wires=2)
    qml.Toffoli(wires=(0,1,"aux"))
    
    qml.expval(qml.PauliZ("aux"))
    qml.state()


>>> print(draw_text(tape))
  0: ─╭QFT──RX─╭C─┤     State
  1: ─├QFT──RY─├C─┤     State
  2: ─╰QFT──RZ─│──┤     State
aux: ──────────╰X─┤ <Z> State

```

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
josh146: > Users can inspect specific parameters via other means.

@albi3ro does the new circuit drawer preserve the ability to print _scalar_ parameters, e.g.,

```python
>>> print(draw_text(tape))
  0: ─╭QFT──RX(0.1)─╭C─┤     State
  1: ─├QFT──RY(0.2)─├C─┤     State
  2: ─╰QFT──RZ(0.3)─│──┤     State
aux: ───────────────╰X─┤ <Z> State
```

I would say that this is a very important feature, since we use this to visualize circuit diagrams with rotation parameters across the docs + blog + demos!
albi3ro: @josh146 Yes it can print scaler parameters via something like `draw_text(tape, decimals=2)`.  It just won't print out matrices.  Though we could add that in if people really want it.
josh146: Hey all, just curious on the timeframe for this PR?
albi3ro: @josh146  I'm going to make it my first priority next quarter.  Mostly just test writing at this point, but those are going to be extensive. It could probably use a round of pre-review if anyone has the time.
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1885](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (330126b) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/e7635a33e055f1e793b85f763de660706eff3a0b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (e7635a3) will **decrease** coverage by `2.11%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@            Coverage Diff             @@
##           master    #1885      +/-   ##
==========================================
- Coverage   99.18%   97.07%   -2.12%     
==========================================
  Files         226      227       +1     
  Lines       17388    17469      +81     
==========================================
- Hits        17247    16958     -289     
- Misses        141      511     +370     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/drawer/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RyYXdlci9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [pennylane/drawer/tape\_text.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RyYXdlci90YXBlX3RleHQucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/operation.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wZXJhdGlvbi5weQ==) | `95.76% <100.00%> (ø)` | |
| [pennylane/ops/qubit/hamiltonian.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC9oYW1pbHRvbmlhbi5weQ==) | `99.04% <100.00%> (ø)` | |
| [pennylane/ops/qubit/non\_parametric\_ops.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC9ub25fcGFyYW1ldHJpY19vcHMucHk=) | `97.36% <100.00%> (ø)` | |
| [pennylane/interfaces/batch/jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvYmF0Y2gvamF4LnB5) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pennylane/interfaces/jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvamF4LnB5) | `0.00% <0.00%> (-97.44%)` | :arrow_down: |
| [pennylane/devices/default\_qubit\_jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdF9qYXgucHk=) | `0.00% <0.00%> (-96.63%)` | :arrow_down: |
| [pennylane/qnn/cost.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3Fubi9jb3N0LnB5) | `42.85% <0.00%> (-57.15%)` | :arrow_down: |
| [pennylane/transforms/classical\_jacobian.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvY2xhc3NpY2FsX2phY29iaWFuLnB5) | `80.00% <0.00%> (-20.00%)` | :arrow_down: |
| ... and [18 more](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [e7635a3...330126b](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1885?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

dime10: Something I noticed while comparing the old drawer to the new, is the change in the `qml.PauliRot` representation deliberate? :)

```
0: ─╭QFT──X─╭C─╭X─────╭R(XYY)─┤ ╭<X@Y>
1: ─├QFT────│──├C──RX─├R(XYY)─┤ ╰<X@Y>
2: ─╰QFT────╰X─╰C─────╰R(XYY)─┤  Probs
 0: ──╭QFT──X──╭C──╭X───────────────╭RX(0.1)──╭┤ ⟨X ⊗ Y⟩ 
 1: ──├QFT─────│───├C──RX(0.00335)──├RY(0.1)──╰┤ ⟨X ⊗ Y⟩ 
 2: ──╰QFT─────╰X──╰C───────────────╰RY(0.1)───┤ Probs   
```

Another thing I'm curious about is the cability of drawing multiple measurement layers. Was this added with complex measurement processes in mind or for practical reasons? 
albi3ro: [sc-12939]
Jaybsoni: Just another thing I came across when playing around with it. There are two features of the old text drawer that don't pass over: the numbering of `QubitUnitary` operations don't show up and the basis with respect to which we `sample` our circuit isn't specified. 

I think the first feature is nice to have since we now omit showing the unitary matrices; if they are labeled the user can at least keep track of them and see if / where they repeat in the circuit. 

The second feature would also be a nice to have as it just provides more info in general, but maybe not fully required. 

Here is an example of what the current behaviour is: 
<img width="1043" alt="image" src="https://user-images.githubusercontent.com/21964864/150732658-c4cf3225-ffb7-461f-9fa1-1a277dfc1a61.png">


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index fb333c8a7..07da939ec 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -259,6 +259,11 @@
 
 <h3>Improvements</h3>
 
+* The new function `qml.drawer.tape_text` produces a string drawing of a tape. This function
+  differs in implementation and minor stylistic details from the old string circuit drawing
+  infrastructure.
+  [(#1885)](https://github.com/PennyLaneAI/pennylane/pull/1885)
+
 * The `RotosolveOptimizer` now raises an error if no trainable arguments are
   detected, instead of silently skipping update steps for all arguments.
   [(#2109)](https://github.com/PennyLaneAI/pennylane/pull/2109)
diff --git a/pennylane/drawer/__init__.py b/pennylane/drawer/__init__.py
index f17221053..3f328f559 100644
--- a/pennylane/drawer/__init__.py
+++ b/pennylane/drawer/__init__.py
@@ -20,6 +20,7 @@ This module provides the circuit drawing functionality used to display circuits
 
 """
 
+from .tape_text import tape_text
 from .circuit_drawer import CircuitDrawer
 from .charsets import CHARSETS
 from .tape_mpl import tape_mpl
diff --git a/pennylane/drawer/tape_text.py b/pennylane/drawer/tape_text.py
new file mode 100644
index 000000000..96d81e973
--- /dev/null
+++ b/pennylane/drawer/tape_text.py
@@ -0,0 +1,270 @@
+# Copyright 2018-2021 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+This module contains logic for the text based circuit drawer through the ``tape_text`` function.
+"""
+
+import pennylane as qml
+from pennylane.operation import Expectation, Probability, Sample, Variance, State
+
+from .drawable_layers import drawable_layers
+from .utils import convert_wire_order
+
+
+def _add_grouping_symbols(op, layer_str, wire_map):
+    """Adds symbols indicating the extent of a given object."""
+    if len(op.wires) > 1:
+        mapped_wires = [wire_map[w] for w in op.wires]
+        min_w, max_w = min(mapped_wires), max(mapped_wires)
+        layer_str[min_w] = "╭"
+        layer_str[max_w] = "╰"
+
+        for w in range(min_w + 1, max_w):
+            layer_str[w] = "├" if w in mapped_wires else "│"
+
+    return layer_str
+
+
+def _add_op(op, layer_str, wire_map, decimals):
+    """Updates ``layer_str`` with ``op`` operation."""
+    layer_str = _add_grouping_symbols(op, layer_str, wire_map)
+
+    control_wires = op.control_wires
+    for w in control_wires:
+        layer_str[wire_map[w]] += "C"
+
+    label = op.label(decimals=decimals).replace("\n", "")
+    for w in op.wires:
+        if w not in control_wires:
+            layer_str[wire_map[w]] += label
+
+    return layer_str
+
+
+measurement_label_map = {
+    Expectation: lambda label: f"<{label}>",
+    Probability: lambda label: f"Probs[{label}]" if label else "Probs",
+    Sample: lambda label: f"Sample[{label}]" if label else "Sample",
+    Variance: lambda label: f"Var[{label}]",
+    State: lambda label: "State",
+}
+
+
+def _add_measurement(m, layer_str, wire_map, decimals):
+    """Updates ``layer_str`` with the ``m`` measurement."""
+    layer_str = _add_grouping_symbols(m, layer_str, wire_map)
+
+    obs_label = None if m.obs is None else m.obs.label(decimals=decimals).replace("\n", "")
+    meas_label = measurement_label_map[m.return_type](obs_label)
+
+    if len(m.wires) == 0:  # state or probability across all wires
+        for i, s in enumerate(layer_str):
+            layer_str[i] = s + meas_label
+
+    for w in m.wires:
+        layer_str[wire_map[w]] += meas_label
+    return layer_str
+
+
+# pylint: disable=too-many-arguments
+def tape_text(
+    tape, wire_order=None, show_all_wires=False, decimals=None, max_length=100, cache=None
+):
+    """Text based diagram for a Quantum Tape.
+
+    Args:
+        tape (QuantumTape): the operations and measurements to draw
+
+    Keyword Args:
+        wire_order (Sequence[Any]): the order (from top to bottom) to print the wires of the circuit
+        show_all_wires (bool): If True, all wires, including empty wires, are printed.
+        decimals (int): How many decimal points to include when formatting operation parameters.
+            Default ``None`` will omit parameters from operation labels.
+        max_length (Int) : Maximum length of a individual line.  After this length, the diagram will
+            begin anew beneath the previous lines.
+        cache (dict): Used to store information between recursive calls. Currently only used for
+            numbering nested tapes.
+
+    Returns:
+        str : String based graphic of the circuit.
+
+    **Example:**
+
+    .. code-block:: python
+
+        with qml.tape.QuantumTape() as tape:
+            qml.QFT(wires=(0,1,2))
+            qml.RX(1.234, wires=0)
+            qml.RY(1.234, wires=1)
+            qml.RZ(1.234, wires=2)
+            qml.Toffoli(wires=(0,1,"aux"))
+
+            qml.expval(qml.PauliZ("aux"))
+            qml.var(qml.PauliZ(0) @ qml.PauliZ(1))
+            qml.probs(wires=(0,1,2,"aux"))
+
+    >>> print(tape_text(tape))
+      0: ─╭QFT──RX─╭C─┤ ╭Var[Z@Z] ╭Probs
+      1: ─├QFT──RY─├C─┤ ╰Var[Z@Z] ├Probs
+      2: ─╰QFT──RZ─│──┤           ├Probs
+    aux: ──────────╰X─┤  <Z>      ╰Probs
+
+    .. UsageDetails::
+
+    By default, parameters are omitted. By specifying the ``decimals`` keyword, parameters
+    are displayed to the specified precision. Matrix-valued parameters are never displayed.
+
+    >>> print(tape_text(tape, decimals=2))
+      0: ─╭QFT──RX(1.23)─╭C─┤ ╭Var[Z@Z] ╭Probs
+      1: ─├QFT──RY(1.23)─├C─┤ ╰Var[Z@Z] ├Probs
+      2: ─╰QFT──RZ(1.23)─│──┤           ├Probs
+    aux: ────────────────╰X─┤  <Z>      ╰Probs
+
+
+    The ``max_length`` keyword wraps long circuits:
+
+    .. code-block:: python
+
+        rng = np.random.default_rng(seed=42)
+        shape = qml.StronglyEntanglingLayers.shape(n_wires=5, n_layers=5)
+        params = rng.random(shape)
+        tape2 = qml.StronglyEntanglingLayers(params, wires=range(5)).expand()
+        print(tape_text(tape2, max_length=60))
+
+
+    .. code-block:: none
+
+        0: ──Rot─╭C──────────╭X──Rot─╭C───────╭X──Rot──────╭C────╭X
+        1: ──Rot─╰X─╭C───────│───Rot─│──╭C────│──╭X────Rot─│──╭C─│─
+        2: ──Rot────╰X─╭C────│───Rot─╰X─│──╭C─│──│─────Rot─│──│──╰C
+        3: ──Rot───────╰X─╭C─│───Rot────╰X─│──╰C─│─────Rot─╰X─│────
+        4: ──Rot──────────╰X─╰C──Rot───────╰X────╰C────Rot────╰X───
+
+        ───Rot───────────╭C─╭X──Rot──────╭C──────────────╭X─┤
+        ──╭X────Rot──────│──╰C─╭X────Rot─╰X───╭C─────────│──┤
+        ──│────╭X────Rot─│─────╰C───╭X────Rot─╰X───╭C────│──┤
+        ──╰C───│─────Rot─│──────────╰C───╭X────Rot─╰X─╭C─│──┤
+        ───────╰C────Rot─╰X──────────────╰C────Rot────╰X─╰C─┤
+
+
+    The ``wire_order`` keyword specifies the order of the wires from
+    top to bottom:
+
+    >>> print(tape_text(tape, wire_order=["aux", 2, 1, 0]))
+    aux: ──────────╭X─┤  <Z>      ╭Probs
+      2: ─╭QFT──RZ─│──┤           ├Probs
+      1: ─├QFT──RY─├C─┤ ╭Var[Z@Z] ├Probs
+      0: ─╰QFT──RX─╰C─┤ ╰Var[Z@Z] ╰Probs
+
+    If the wire order contains empty wires, they are only shown if the ``show_all_wires=True``.
+
+    >>> print(tape_text(tape, wire_order=["a", "b", "aux", 0,1,2], show_all_wires=True))
+      a: ─────────────┤
+      b: ─────────────┤
+    aux: ──────────╭X─┤  <Z>      ╭Probs
+      0: ─╭QFT──RX─├C─┤ ╭Var[Z@Z] ├Probs
+      1: ─├QFT──RY─╰C─┤ ╰Var[Z@Z] ├Probs
+      2: ─╰QFT──RZ────┤           ╰Probs
+
+    When the provided tape has nested tapes inside, this function is called recursively.
+    To maintain numbering of tapes to arbitrary levels of nesting, the ``cache`` keyword
+    uses the ``"tape_offset"`` value to determine numbering. Note that the value is updated
+    during the call.
+
+    .. code-block:: python
+
+        with qml.tape.QuantumTape() as tape:
+            with qml.tape.QuantumTape() as tape_inner:
+                qml.PauliX(0)
+
+        cache = {'tape_offset': 3}
+        print(tape_text(tape, cache=cache))
+        print("New tape offset: ", cache['tape_offset'])
+
+
+    .. code-block:: none
+
+        0: ──Tape:3─┤
+
+        Tape:3
+        0: ──X─┤
+        New tape offset:  4
+
+    """
+    if cache is None:
+        cache = {"tape_offset": 0}
+    tape_cache = []
+
+    wire_map = convert_wire_order(
+        tape.operations + tape.measurements, wire_order=wire_order, show_all_wires=show_all_wires
+    )
+    n_wires = len(wire_map)
+    if n_wires == 0:
+        return ""
+
+    totals = [f"{wire}: " for wire in wire_map]
+    line_length = max(len(s) for s in totals)
+    totals = [s.rjust(line_length, " ") for s in totals]
+
+    # Used to store lines that are hitting the maximum length
+    finished_lines = []
+
+    layers_list = [
+        drawable_layers(tape.operations, wire_map=wire_map),
+        drawable_layers(tape.measurements, wire_map=wire_map),
+    ]
+    add_list = [_add_op, _add_measurement]
+    fillers = ["─", " "]
+    enders = [True, False]  # add "─┤" after all operations
+
+    for layers, add, filler, ender in zip(layers_list, add_list, fillers, enders):
+
+        for layer in layers:
+            layer_str = [filler] * n_wires
+
+            for op in layer:
+                if isinstance(op, qml.tape.QuantumTape):
+                    layer_str = _add_grouping_symbols(op, layer_str, wire_map)
+                    label = f"Tape:{cache['tape_offset']+len(tape_cache)}"
+                    for w in op.wires:
+                        layer_str[wire_map[w]] += label
+                    tape_cache.append(op)
+                else:
+                    layer_str = add(op, layer_str, wire_map, decimals)
+
+            max_label_len = max(len(s) for s in layer_str)
+            layer_str = [s.ljust(max_label_len, filler) for s in layer_str]
+
+            line_length += max_label_len + 1  # one for the filler character
+            if line_length > max_length:
+                # move totals into finished_lines and reset totals
+                finished_lines += totals
+                finished_lines[-1] += "\n"
+                totals = [filler] * n_wires
+                line_length = 1 + max_label_len
+
+            totals = [filler.join([t, s]) for t, s in zip(totals, layer_str)]
+        if ender:
+            totals = [s + "─┤" for s in totals]
+
+    # Recursively handle nested tapes #
+    tape_totals = "\n".join(finished_lines + totals)
+    current_tape_offset = cache["tape_offset"]
+    cache["tape_offset"] += len(tape_cache)
+    for i, nested_tape in enumerate(tape_cache):
+        label = f"\nTape:{i+current_tape_offset}"
+        tape_str = tape_text(nested_tape, wire_order, show_all_wires, decimals, max_length, cache)
+        tape_totals = "\n".join([tape_totals, label, tape_str])
+
+    return tape_totals
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 79dd3b840..c643ff7b3 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -1154,9 +1154,9 @@ class Tensor(Observable):
 
         >>> T = qml.PauliX(0) @ qml.Hadamard(2)
         >>> T.label()
-        'X⊗H'
+        'X@H'
         >>> T.label(base_label=["X0", "H2"])
-        'X0⊗H2'
+        'X0@H2'
 
         """
         if base_label is not None:
@@ -1165,11 +1165,11 @@ class Tensor(Observable):
                     "Tensor label requires ``base_label`` keyword to be same length"
                     " as tensor components."
                 )
-            return "⊗".join(
+            return "@".join(
                 ob.label(decimals=decimals, base_label=lbl) for ob, lbl in zip(self.obs, base_label)
             )
 
-        return "⊗".join(ob.label(decimals=decimals) for ob in self.obs)
+        return "@".join(ob.label(decimals=decimals) for ob in self.obs)
 
     def queue(self, context=qml.QueuingContext, init=False):  # pylint: disable=arguments-differ
         constituents = self.obs
diff --git a/pennylane/ops/qubit/hamiltonian.py b/pennylane/ops/qubit/hamiltonian.py
index 29d2f7c93..a91a4c789 100644
--- a/pennylane/ops/qubit/hamiltonian.py
+++ b/pennylane/ops/qubit/hamiltonian.py
@@ -211,6 +211,7 @@ class Hamiltonian(Observable):
         super().__init__(*coeffs_flat, wires=self._wires, id=id, do_queue=do_queue)
 
     def label(self, decimals=None, base_label=None):
+        decimals = None if (len(self.parameters) > 3) else decimals
         return super().label(decimals=decimals, base_label=base_label or "𝓗")
 
     @property
diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py
index bebee1443..4a5374b44 100644
--- a/pennylane/ops/qubit/non_parametric_ops.py
+++ b/pennylane/ops/qubit/non_parametric_ops.py
@@ -441,7 +441,7 @@ class CNOT(Operation):
     matrix = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
 
     def label(self, decimals=None, base_label=None):
-        return base_label or "⊕"
+        return base_label or "X"
 
     @classmethod
     def _matrix(cls, *params):
@@ -821,7 +821,7 @@ class Toffoli(Operation):
     )
 
     def label(self, decimals=None, base_label=None):
-        return base_label or "⊕"
+        return base_label or "X"
 
     @classmethod
     def _matrix(cls, *params):
@@ -967,7 +967,7 @@ class MultiControlledX(Operation):
         return self._control_wires
 
     def label(self, decimals=None, base_label=None):
-        return base_label or "⊕"
+        return base_label or "X"
 
     @staticmethod
     def _parse_control_values(control_wires, control_values):
diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py
index ef5de9319..f5399e81a 100644
--- a/pennylane/ops/qubit/parametric_ops.py
+++ b/pennylane/ops/qubit/parametric_ops.py
@@ -599,14 +599,14 @@ class PauliRot(Operation):
 
         >>> op = qml.PauliRot(0.1, "XYY", wires=(0,1,2))
         >>> op.label()
-        'R(XYY)'
+        'RXYY'
         >>> op.label(decimals=2)
-        'R(XYY)\n(0.10)'
+        'RXYY\n(0.10)'
         >>> op.label(base_label="PauliRot")
         'PauliRot\n(0.10)'
 
         """
-        op_label = base_label or ("R(" + self.parameters[1] + ")")
+        op_label = base_label or ("R" + self.parameters[1])
 
         if self.inverse:
             op_label += "⁻¹"


Test patch:
diff --git a/tests/drawer/test_tape_text.py b/tests/drawer/test_tape_text.py
new file mode 100644
index 000000000..a8a2be0ca
--- /dev/null
+++ b/tests/drawer/test_tape_text.py
@@ -0,0 +1,385 @@
+# Copyright 2018-2020 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Unit tests for the `pennylane.draw_text` function.
+"""
+
+import pytest
+import pennylane as qml
+
+from pennylane.drawer import tape_text
+from pennylane.drawer.tape_text import _add_grouping_symbols, _add_op, _add_measurement
+from pennylane.tape import QuantumTape
+
+default_wire_map = {0: 0, 1: 1, 2: 2, 3: 3}
+
+with QuantumTape() as tape:
+    qml.RX(1.23456, wires=0)
+    qml.RY(2.3456, wires="a")
+    qml.RZ(3.4567, wires=1.234)
+
+
+class TestHelperFunctions:
+    @pytest.mark.parametrize(
+        "op, out",
+        [
+            (qml.PauliX(0), ["", "", "", ""]),
+            (qml.CNOT(wires=(0, 2)), ["╭", "│", "╰", ""]),
+            (qml.CSWAP(wires=(0, 2, 3)), ["╭", "│", "├", "╰"]),
+        ],
+    )
+    def test_add_grouping_symbols(self, op, out):
+        assert out == _add_grouping_symbols(op, ["", "", "", ""], default_wire_map)
+
+    @pytest.mark.parametrize(
+        "op, out",
+        [
+            (qml.expval(qml.PauliX(0)), ["<X>", "", "", ""]),
+            (qml.probs(wires=(0, 2)), ["╭Probs", "│", "╰Probs", ""]),
+            (qml.var(qml.PauliX(1)), ["", "Var[X]", "", ""]),
+            (qml.state(), ["State", "State", "State", "State"]),
+            (qml.sample(), ["Sample", "Sample", "Sample", "Sample"]),
+        ],
+    )
+    def test_add_measurements(self, op, out):
+        """Test private _add_measurement function renders as expected."""
+        assert out == _add_measurement(op, [""] * 4, default_wire_map, None)
+
+    @pytest.mark.parametrize(
+        "op, out",
+        [
+            (qml.PauliX(0), ["─X", "─", "─", "─"]),
+            (qml.CNOT(wires=(0, 2)), ["╭C", "│", "╰X", "─"]),
+            (qml.Toffoli(wires=(0, 1, 3)), ["╭C", "├C", "│", "╰X"]),
+            (qml.IsingXX(1.23, wires=(0, 2)), ["╭IsingXX", "│", "╰IsingXX", "─"]),
+        ],
+    )
+    def test_add_op(self, op, out):
+        """Test adding the first operation to array of strings"""
+        assert out == _add_op(op, ["─"] * 4, default_wire_map, None)
+
+    @pytest.mark.parametrize(
+        "op, out",
+        [
+            (qml.PauliY(1), ["─X", "─Y", "─", "─"]),
+            (qml.CNOT(wires=(1, 2)), ["─X", "╭C", "╰X", "─"]),
+            (qml.CRX(1.23, wires=(2, 3)), ["─X", "─", "╭C", "╰RX"]),
+        ],
+    )
+    def test_add_second_op(self, op, out):
+        """Test adding a second operation to the array of strings"""
+        start = _add_op(qml.PauliX(0), ["─"] * 4, default_wire_map, None)
+        assert out == _add_op(op, start, default_wire_map, None)
+
+
+class TestEmptyTapes:
+    def test_empty_tape(self):
+        """Test using an empty tape returns a blank string"""
+        assert tape_text(QuantumTape()) == ""
+
+    def test_empty_tape_wire_order(self):
+        """Test wire order and show_all_wires shows wires with empty tape."""
+        expected = "a: ───┤  \nb: ───┤  "
+        out = tape_text(QuantumTape(), wire_order=["a", "b"], show_all_wires=True)
+        assert expected == out
+
+
+class TestLabeling:
+    def test_any_wire_labels(self):
+        """Test wire labels with different kinds of objects."""
+
+        split_str = tape_text(tape).split("\n")
+        assert split_str[0][0:6] == "    0:"
+        assert split_str[1][0:6] == "    a:"
+        assert split_str[2][0:6] == "1.234:"
+
+    def test_wire_order(self):
+        """Test wire_order keyword changes order of the wires"""
+
+        split_str = tape_text(tape, wire_order=[1.234, "a", 0, "b"]).split("\n")
+        assert split_str[2][0:6] == "    0:"
+        assert split_str[1][0:6] == "    a:"
+        assert split_str[0][0:6] == "1.234:"
+
+    def test_show_all_wires(self):
+        """Test wire_order constains unused wires, show_all_wires
+        forces them to display."""
+
+        split_str = tape_text(tape, wire_order=["b"], show_all_wires=True).split("\n")
+
+        assert split_str[0][0:6] == "    b:"
+        assert split_str[1][0:6] == "    0:"
+        assert split_str[2][0:6] == "    a:"
+        assert split_str[3][0:6] == "1.234:"
+
+
+class TestDecimals:
+    """Test the decimals keyword argument."""
+
+    def test_decimals(self):
+        """Test that the decimals keyword makes the operation parameters included."""
+
+        expected = "    0: ──RX(1.23)─┤  \n    a: ──RY(2.35)─┤  \n1.234: ──RZ(3.46)─┤  "
+
+        assert tape_text(tape, decimals=2) == expected
+
+    def test_decimals_multiparameters(self):
+        """Tests decimals also displays parameters when the operation has multiple parameters."""
+
+        with QuantumTape() as tape_rot:
+            qml.Rot(1.2345, 2.3456, 3.4566, wires=0)
+
+        expected = "0: ──Rot(1.23,2.35,3.46)─┤  "
+        assert tape_text(tape_rot, decimals=2) == expected
+
+    def test_decimals_0(self):
+        """Test decimals=0 rounds to integers"""
+
+        expected = "    0: ──RX(1)─┤  \n" "    a: ──RY(2)─┤  \n" "1.234: ──RZ(3)─┤  "
+
+        assert tape_text(tape, decimals=0) == expected
+
+    def test_torch_parameters(self):
+        """Test torch parameters in tape display as normal numbers."""
+        torch = pytest.importorskip("torch")
+        with QuantumTape() as tape_torch:
+            qml.Rot(torch.tensor(1.234), torch.tensor(2.345), torch.tensor(3.456), wires=0)
+
+        expected = "0: ──Rot(1.23,2.35,3.46)─┤  "
+        assert tape_text(tape_torch, decimals=2) == expected
+
+    def test_tensorflow_parameters(self):
+        """Test tensorflow parameters display as normal numbers."""
+        tf = pytest.importorskip("tensorflow")
+        with QuantumTape() as tape_tf:
+            qml.Rot(tf.Variable(1.234), tf.Variable(2.345), tf.Variable(3.456), wires=0)
+
+        expected = "0: ──Rot(1.23,2.35,3.46)─┤  "
+        assert tape_text(tape_tf, decimals=2) == expected
+
+    def test_jax_parameters(self):
+        """Test jax parameters in tape display as normal numbers."""
+        jnp = pytest.importorskip("jax.numpy")
+
+        with QuantumTape() as tape_jax:
+            qml.Rot(jnp.array(1.234), jnp.array(2.345), jnp.array(3.456), wires=0)
+
+        expected = "0: ──Rot(1.23,2.35,3.46)─┤  "
+        assert tape_text(tape_jax, decimals=2) == expected
+
+
+class TestMaxLength:
+    """Test the max_length keyword."""
+
+    def test_max_length_default(self):
+        """Test max length defaults to 100."""
+        with QuantumTape() as tape_ml:
+            for _ in range(50):
+                qml.PauliX(0)
+                qml.PauliY(1)
+
+            for _ in range(3):
+                qml.sample()
+
+        out = tape_text(tape_ml)
+        assert 95 <= max(len(s) for s in out.split("\n")) <= 100
+
+    @pytest.mark.parametrize("ml", [10, 15, 20])
+    def test_setting_max_length(self, ml):
+        """Test several custom max_length parameters change the wrapping length."""
+
+        with QuantumTape() as tape_ml:
+            for _ in range(10):
+                qml.PauliX(0)
+                qml.PauliY(1)
+
+            for _ in range(3):
+                qml.sample()
+
+        out = tape_text(tape, max_length=ml)
+        assert max(len(s) for s in out.split("\n")) <= ml
+
+
+single_op_tests_data = [
+    (qml.CNOT(wires=(0, 1)), "0: ─╭C─┤  \n1: ─╰X─┤  "),
+    (qml.Toffoli(wires=(0, 1, 2)), "0: ─╭C─┤  \n1: ─├C─┤  \n2: ─╰X─┤  "),
+    (qml.Barrier(wires=(0, 1, 2)), "0: ─╭||─┤  \n1: ─├||─┤  \n2: ─╰||─┤  "),
+    (qml.CSWAP(wires=(0, 1, 2)), "0: ─╭C────┤  \n1: ─├SWAP─┤  \n2: ─╰SWAP─┤  "),
+    (
+        qml.DoubleExcitationPlus(1.23, wires=(0, 1, 2, 3)),
+        "0: ─╭G²₊(1.23)─┤  \n1: ─├G²₊(1.23)─┤  \n2: ─├G²₊(1.23)─┤  \n3: ─╰G²₊(1.23)─┤  ",
+    ),
+    (qml.QubitUnitary(qml.numpy.eye(4), wires=(0, 1)), "0: ─╭U─┤  \n1: ─╰U─┤  "),
+    (qml.QubitSum(wires=(0, 1, 2)), "0: ─╭Σ─┤  \n1: ─├Σ─┤  \n2: ─╰Σ─┤  "),
+    (qml.AmplitudeDamping(0.98, wires=0), "0: ──AmplitudeDamping(0.98)─┤  "),
+    (
+        qml.QubitStateVector([0, 1, 0, 0], wires=(0, 1)),
+        "0: ─╭QubitStateVector─┤  \n1: ─╰QubitStateVector─┤  ",
+    ),
+    (qml.Kerr(1.234, wires=0), "0: ──Kerr(1.23)─┤  "),
+    (
+        qml.GroverOperator(wires=(0, 1, 2)),
+        "0: ─╭GroverOperator─┤  \n1: ─├GroverOperator─┤  \n2: ─╰GroverOperator─┤  ",
+    ),
+    (qml.RX(1.234, wires=0).inv(), "0: ──RX⁻¹(1.23)─┤  "),
+    (qml.expval(qml.PauliZ(0)), "0: ───┤  <Z>"),
+    (qml.var(qml.PauliZ(0)), "0: ───┤  Var[Z]"),
+    (qml.probs(wires=0), "0: ───┤  Probs"),
+    (qml.probs(op=qml.PauliZ(0)), "0: ───┤  Probs[Z]"),
+    (qml.sample(wires=0), "0: ───┤  Sample"),
+    (qml.sample(op=qml.PauliX(0)), "0: ───┤  Sample[X]"),
+    (qml.expval(0.1 * qml.PauliX(0) @ qml.PauliY(1)), "0: ───┤ ╭<𝓗(0.10)>\n1: ───┤ ╰<𝓗(0.10)>"),
+    (
+        qml.expval(
+            0.1 * qml.PauliX(0) + 0.2 * qml.PauliY(1) + 0.3 * qml.PauliZ(0) + 0.4 * qml.PauliZ(1)
+        ),
+        "0: ───┤ ╭<𝓗>\n1: ───┤ ╰<𝓗>",
+    ),
+]
+
+
+@pytest.mark.parametrize("op, expected", single_op_tests_data)
+def test_single_ops(op, expected):
+    """Tests a variety of different single operation tapes render as expected."""
+
+    with QuantumTape() as tape:
+        qml.apply(op)
+
+    assert tape_text(tape, decimals=2) == expected
+
+
+class TestLayering:
+    """Test operations are placed in the correct locations."""
+
+    def test_adjacent_ops(self):
+        """Test non-blocking gates end up on same layer."""
+
+        with QuantumTape() as tape:
+            qml.PauliX(0)
+            qml.PauliX(1)
+            qml.PauliX(2)
+
+        assert tape_text(tape) == "0: ──X─┤  \n1: ──X─┤  \n2: ──X─┤  "
+
+    def test_blocking_ops(self):
+        """Test single qubit gates on same wire line up."""
+
+        with QuantumTape() as tape:
+            qml.PauliX(0)
+            qml.PauliX(0)
+            qml.PauliX(0)
+
+        assert tape_text(tape) == "0: ──X──X──X─┤  "
+
+    def test_blocking_multiwire_gate(self):
+        """Tests gate gets blocked by multi-wire gate."""
+
+        with QuantumTape() as tape:
+            qml.PauliX(0)
+            qml.IsingXX(1.2345, wires=(0, 2))
+            qml.PauliX(1)
+
+        expected = "0: ──X─╭IsingXX────┤  \n1: ────│─────────X─┤  \n2: ────╰IsingXX────┤  "
+
+        assert tape_text(tape, wire_order=[0, 1, 2]) == expected
+
+
+class TestNestedTapes:
+    """Test situations with nested tapes."""
+
+    def test_cache_keyword_tape_offset(self):
+        """Test that tape numbering is determined by the `tape_offset` keyword of the cache."""
+
+        with QuantumTape() as tape:
+            with QuantumTape() as tape_inner:
+                qml.PauliX(0)
+
+        expected = "0: ──Tape:3─┤  \n" "\nTape:3\n" "0: ──X─┤  "
+
+        assert tape_text(tape, cache={"tape_offset": 3}) == expected
+
+    def test_multiple_nested_tapes(self):
+        """Test numbers consistent with multiple nested tapes and
+        multiple levels of nesting."""
+
+        with QuantumTape() as tape:
+            qml.PauliX(0)
+            with QuantumTape() as tape0:
+                qml.PauliY(0)
+                qml.PauliZ(0)
+                with QuantumTape() as tape2:
+                    qml.PauliX(0)
+            with QuantumTape() as tape1:
+                qml.PauliY(0)
+                with QuantumTape() as tape3:
+                    qml.PauliZ(0)
+
+        expected = (
+            "0: ──X──Tape:0──Tape:1─┤  \n"
+            "\nTape:0\n"
+            "0: ──Y──Z──Tape:2─┤  \n"
+            "\nTape:2\n"
+            "0: ──X─┤  \n"
+            "\nTape:1\n"
+            "0: ──Y──Tape:3─┤  \n"
+            "\nTape:3\n"
+            "0: ──Z─┤  "
+        )
+
+        assert tape_text(tape) == expected
+
+    def test_nested_tapes_decimals(self):
+        """Test decimals keyword passed to nested tapes."""
+
+        with QuantumTape() as tape:
+            qml.RX(1.2345, wires=0)
+            with QuantumTape() as tape0:
+                qml.Rot(1.2345, 2.3456, 3.456, wires=0)
+
+        expected = "0: ──RX(1.2)──Tape:0─┤  \n" "\nTape:0\n" "0: ──Rot(1.2,2.3,3.5)─┤  "
+
+        assert tape_text(tape, decimals=1) == expected
+
+    def test_nested_tapes_wire_order(self):
+        """Test wire order preserved in nested tapes."""
+
+        with QuantumTape() as tape:
+            qml.PauliX(0)
+            qml.PauliY(1)
+            with QuantumTape() as tape0:
+                qml.PauliX(0)
+                qml.PauliY(1)
+
+        expected = (
+            "1: ──Y─╭Tape:0─┤  \n" "0: ──X─╰Tape:0─┤  \n" "\nTape:0\n" "1: ──Y─┤  \n0: ──X─┤  "
+        )
+
+        assert tape_text(tape, wire_order=[1, 0]) == expected
+
+    def test_nested_tapes_max_length(self):
+        """Test max length passes to recursive tapes."""
+
+        with QuantumTape() as tape:
+            qml.PauliX(0)
+            with QuantumTape() as tape0:
+                for _ in range(10):
+                    qml.PauliX(0)
+
+        expected = (
+            "0: ──X──Tape:0─┤  \n" "\nTape:0\n" "0: ──X──X──X──X──X\n" "\n───X──X──X──X──X─┤  "
+        )
+
+        out = tape_text(tape, max_length=20)
+        assert out == expected
+        assert max(len(s) for s in out.split("\n")) <= 20
diff --git a/tests/ops/qubit/test_hamiltonian.py b/tests/ops/qubit/test_hamiltonian.py
index 779e7ee5d..efbe8543b 100644
--- a/tests/ops/qubit/test_hamiltonian.py
+++ b/tests/ops/qubit/test_hamiltonian.py
@@ -550,8 +550,21 @@ class TestHamiltonian:
         assert set(H.wires) == set([w for op in H.ops for w in op.wires])
 
     def test_label(self):
+        """Tests the label method of Hamiltonian when <=3 coefficients."""
         H = qml.Hamiltonian((-0.8,), (qml.PauliZ(0),))
         assert H.label() == "𝓗"
+        assert H.label(decimals=2) == "𝓗\n(-0.80)"
+
+    def test_label_many_coefficients(self):
+        """Tests the label method of Hamiltonian when >3 coefficients."""
+        H = (
+            0.1 * qml.PauliX(0)
+            + 0.1 * qml.PauliY(1)
+            + 0.3 * qml.PauliZ(0) @ qml.PauliX(1)
+            + 0.4 * qml.PauliX(3)
+        )
+        assert H.label() == "𝓗"
+        assert H.label(decimals=2) == "𝓗"
 
     @pytest.mark.parametrize("terms, string", zip(valid_hamiltonians, valid_hamiltonians_str))
     def test_hamiltonian_str(self, terms, string):
diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py
index f200b4894..75efc095b 100644
--- a/tests/ops/qubit/test_non_parametric_ops.py
+++ b/tests/ops/qubit/test_non_parametric_ops.py
@@ -866,7 +866,7 @@ label_data = [
     (qml.S(wires=0), "S", "S⁻¹"),
     (qml.T(wires=0), "T", "T⁻¹"),
     (qml.SX(wires=0), "SX", "SX⁻¹"),
-    (qml.CNOT(wires=(0, 1)), "⊕", "⊕"),
+    (qml.CNOT(wires=(0, 1)), "X", "X"),
     (qml.CZ(wires=(0, 1)), "Z", "Z"),
     (qml.CY(wires=(0, 1)), "Y", "Y"),
     (qml.SWAP(wires=(0, 1)), "SWAP", "SWAP⁻¹"),
@@ -874,8 +874,8 @@ label_data = [
     (qml.SISWAP(wires=(0, 1)), "SISWAP", "SISWAP⁻¹"),
     (qml.SQISW(wires=(0, 1)), "SISWAP", "SISWAP⁻¹"),
     (qml.CSWAP(wires=(0, 1, 2)), "SWAP", "SWAP"),
-    (qml.Toffoli(wires=(0, 1, 2)), "⊕", "⊕"),
-    (qml.MultiControlledX(control_wires=(0, 1, 2), wires=(3)), "⊕", "⊕"),
+    (qml.Toffoli(wires=(0, 1, 2)), "X", "X"),
+    (qml.MultiControlledX(control_wires=(0, 1, 2), wires=(3)), "X", "X"),
     (qml.Barrier(0), "||", "||"),
     (qml.WireCut(wires=0), "//", "//"),
 ]
diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py
index 42bea16e9..6d671f11f 100644
--- a/tests/ops/qubit/test_parametric_ops.py
+++ b/tests/ops/qubit/test_parametric_ops.py
@@ -1436,10 +1436,10 @@ label_data = [
     (qml.MultiRZ(1.23456, wires=0), "MultiRZ", "MultiRZ\n(1.23)", "MultiRZ\n(1)", "MultiRZ⁻¹\n(1)"),
     (
         qml.PauliRot(1.2345, "XYZ", wires=(0, 1, 2)),
-        "R(XYZ)",
-        "R(XYZ)\n(1.23)",
-        "R(XYZ)\n(1)",
-        "R(XYZ)⁻¹\n(1)",
+        "RXYZ",
+        "RXYZ\n(1.23)",
+        "RXYZ\n(1)",
+        "RXYZ⁻¹\n(1)",
     ),
     (
         qml.PhaseShift(1.2345, wires=0),
diff --git a/tests/test_operation.py b/tests/test_operation.py
index dcfd0d247..54698d50c 100644
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -469,9 +469,9 @@ class TestTensor:
         y = qml.PauliZ(2)
         T = Tensor(x, y)
 
-        assert T.label() == "X⊗Z"
-        assert T.label(decimals=2) == "X⊗Z"
-        assert T.label(base_label=["X0", "Z2"]) == "X0⊗Z2"
+        assert T.label() == "X@Z"
+        assert T.label(decimals=2) == "X@Z"
+        assert T.label(base_label=["X0", "Z2"]) == "X0@Z2"
 
         with pytest.raises(ValueError, match=r"Tensor label requires"):
             T.label(base_label="nope")