Repository: Qiskit/qiskit-terra (python)

Issue:
Overloading `@` for `Operator` class
### What should we add?

## Proposition

Overloading the `@` operator for class `qiskit.quantum_info.operators.Operator` ([documentation](https://qiskit.org/documentation/stubs/qiskit.quantum_info.Operator.html?highlight=operator#qiskit.quantum_info.Operator)) so it works the same as `Operator.compose()` method. The same behavior is done with class `qiskit.opflow.operator_base.OperatorBase` ([documentation](https://qiskit.org/documentation/stubs/qiskit.opflow.OperatorBase.html#qiskit.opflow.OperatorBase)).

## Motivation

When going through the Open Science Prize 2021 I encountered [problem](https://github.com/qiskit-community/open-science-prize-2021/issues/5). After investigation it turned out that `Operator` class doesn't have `@` overloading, so in order to fix the issue `compose()` or `np.matmul()` methods need to be used. As this is good solution it would be easier to use `@`, especially for reading the code.

Pull request description:
Alias BaseOperator.__matmul__ to BaseOperator.dot
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Summary

Adds a `@` (`__matmul__`) binary operator. (fixes #7502) 

### Details and comments

Adds a `@` (`__matmul__`) binary operator to `BaseOperator` subclasses
in the qiskit.quantum_info module. This is shorthand to call the
classes `dot` method ( `A@ B == A.dot(B)`).





Patch:
diff --git a/qiskit/quantum_info/operators/mixins/group.py b/qiskit/quantum_info/operators/mixins/group.py
index 294e9a070..bdfce2a07 100644
--- a/qiskit/quantum_info/operators/mixins/group.py
+++ b/qiskit/quantum_info/operators/mixins/group.py
@@ -34,6 +34,7 @@ class GroupMixin(ABC):
     And the following operator overloads:
 
         - ``&``, ``__and__`` -> :meth:`compose`
+        - ``@``, ``__matmul__`` -> :meth:`dot`
         - ``^``, ``__xor__`` -> `:meth:`tensor`
         - ``**``, ``__pow__`` -> :meth:`power`
 
@@ -54,6 +55,9 @@ class GroupMixin(ABC):
     def __xor__(self, other):
         return self.tensor(other)
 
+    def __matmul__(self, other):
+        return self.dot(other)
+
     @abstractmethod
     def tensor(self, other):
         r"""Return the tensor product with another CLASS.
@@ -111,9 +115,9 @@ class GroupMixin(ABC):
 
         .. note::
             Composition (``&``) by default is defined as `left` matrix multiplication for
-            matrix operators, while :meth:`dot` is defined as `right` matrix
+            matrix operators, while ``@`` (equivalent to :meth:`dot`) is defined as `right` matrix
             multiplication. That is that ``A & B == A.compose(B)`` is equivalent to
-            ``B.dot(A)`` when ``A`` and ``B`` are of the same type.
+            ``B @ A == B.dot(A)`` when ``A`` and ``B`` are of the same type.
 
             Setting the ``front=True`` kwarg changes this to `right` matrix
             multiplication and is equivalent to the :meth:`dot` method
@@ -131,6 +135,10 @@ class GroupMixin(ABC):
 
         Returns:
             CLASS: The right matrix multiplied CLASS.
+
+        .. note::
+            The dot product can be obtained using the ``@`` binary operator.
+            Hence ``a.dot(b)`` is equivalent to ``a @ b``.
         """
         return self.compose(other, qargs=qargs, front=True)
 
diff --git a/releasenotes/notes/add-overloading@-3fedb7bc2fd4d7f7.yaml b/releasenotes/notes/add-overloading@-3fedb7bc2fd4d7f7.yaml
new file mode 100644
index 000000000..96b8176a9
--- /dev/null
+++ b/releasenotes/notes/add-overloading@-3fedb7bc2fd4d7f7.yaml
@@ -0,0 +1,14 @@
+---
+features:
+  - |
+    Adds a ``@`` (``__matmul__``) binary operator to ``BaseOperator`` subclasses
+    in the :mod:`qiskit.quantum_info` module. This is shorthand to call the
+    classes ``dot`` method (``A @ B == A.dot(B)``).
+
+    Example
+
+    .. code:: python
+    
+      from qiskit.quantum_info import Pauli
+
+      Pauli("X") @ Pauli("Y")


Test patch:
diff --git a/test/python/quantum_info/operators/channel/test_chi.py b/test/python/quantum_info/operators/channel/test_chi.py
index 2d4d281b6..66240f7ce 100644
--- a/test/python/quantum_info/operators/channel/test_chi.py
+++ b/test/python/quantum_info/operators/channel/test_chi.py
@@ -141,7 +141,11 @@ class TestChi(ChannelTestCase):
         chan1 = Chi(chi1, input_dims=2, output_dims=2)
         chan2 = Chi(chi2, input_dims=2, output_dims=2)
         target = rho.evolve(chan1).evolve(chan2)
-        output = rho.evolve(chan2.dot(chan1))
+        chan = chan2.dot(chan1)
+        output = rho.evolve(chan)
+        self.assertEqual(output, target)
+        chan = chan2 @ chan1
+        output = rho.evolve(chan)
         self.assertEqual(output, target)
 
     def test_compose_front(self):
diff --git a/test/python/quantum_info/operators/channel/test_choi.py b/test/python/quantum_info/operators/channel/test_choi.py
index 57851b523..1ef859a9a 100644
--- a/test/python/quantum_info/operators/channel/test_choi.py
+++ b/test/python/quantum_info/operators/channel/test_choi.py
@@ -186,11 +186,13 @@ class TestChoi(ChannelTestCase):
         chan2 = Choi(self.choiY)
         targ = Choi(self.choiZ)
         self.assertEqual(chan1.dot(chan2), targ)
+        self.assertEqual(chan1 @ chan2, targ)
 
         # 50% depolarizing channel
         chan1 = Choi(self.depol_choi(0.5))
         targ = Choi(self.depol_choi(0.75))
         self.assertEqual(chan1.dot(chan1), targ)
+        self.assertEqual(chan1 @ chan1, targ)
 
         # Measure and rotation
         Zp, Zm = np.diag([1, 0]), np.diag([0, 1])
@@ -200,17 +202,23 @@ class TestChoi(ChannelTestCase):
         # X-gate second does nothing
         targ = Choi(np.kron(Zp, Xp) + np.kron(Zm, Xm))
         self.assertEqual(chan2.dot(chan1), targ)
+        self.assertEqual(chan2 @ chan1, targ)
         # X-gate first swaps Z states
         targ = Choi(np.kron(Zm, Xp) + np.kron(Zp, Xm))
         self.assertEqual(chan1.dot(chan2), targ)
+        self.assertEqual(chan1 @ chan2, targ)
 
         # Compose different dimensions
         chan1 = Choi(np.eye(8) / 4, input_dims=2, output_dims=4)
         chan2 = Choi(np.eye(8) / 2, input_dims=4, output_dims=2)
         chan = chan1.dot(chan2)
         self.assertEqual(chan.dim, (4, 4))
+        chan = chan1 @ chan2
+        self.assertEqual(chan.dim, (4, 4))
         chan = chan2.dot(chan1)
         self.assertEqual(chan.dim, (2, 2))
+        chan = chan2 @ chan1
+        self.assertEqual(chan.dim, (2, 2))
 
     def test_compose_front(self):
         """Test front compose method."""
diff --git a/test/python/quantum_info/operators/channel/test_kraus.py b/test/python/quantum_info/operators/channel/test_kraus.py
index 6a7cdf889..5e589878d 100644
--- a/test/python/quantum_info/operators/channel/test_kraus.py
+++ b/test/python/quantum_info/operators/channel/test_kraus.py
@@ -205,11 +205,13 @@ class TestKraus(ChannelTestCase):
         chan2 = Kraus(self.UY)
         targ = rho.evolve(Kraus(self.UZ))
         self.assertEqual(rho.evolve(chan1.dot(chan2)), targ)
+        self.assertEqual(rho.evolve(chan1 @ chan2), targ)
 
         # 50% depolarizing channel
         chan1 = Kraus(self.depol_kraus(0.5))
         targ = rho & Kraus(self.depol_kraus(0.75))
         self.assertEqual(rho.evolve(chan1.dot(chan1)), targ)
+        self.assertEqual(rho.evolve(chan1 @ chan1), targ)
 
         # Compose different dimensions
         kraus1, kraus2 = self.rand_kraus(2, 4, 4), self.rand_kraus(4, 2, 4)
@@ -217,6 +219,7 @@ class TestKraus(ChannelTestCase):
         chan2 = Kraus(kraus2)
         targ = rho & chan1 & chan2
         self.assertEqual(rho.evolve(chan2.dot(chan1)), targ)
+        self.assertEqual(rho.evolve(chan2 @ chan1), targ)
 
     def test_compose_front(self):
         """Test deprecated front compose method."""
diff --git a/test/python/quantum_info/operators/channel/test_ptm.py b/test/python/quantum_info/operators/channel/test_ptm.py
index 6eed780ef..51660e495 100644
--- a/test/python/quantum_info/operators/channel/test_ptm.py
+++ b/test/python/quantum_info/operators/channel/test_ptm.py
@@ -137,6 +137,7 @@ class TestPTM(ChannelTestCase):
         chan2 = PTM(self.ptmY)
         rho_targ = rho.evolve(PTM(self.ptmZ))
         self.assertEqual(rho.evolve(chan2.dot(chan1)), rho_targ)
+        self.assertEqual(rho.evolve(chan2 @ chan1), rho_targ)
 
         # Compose random
         ptm1 = self.rand_matrix(4, 4, real=True)
@@ -145,6 +146,7 @@ class TestPTM(ChannelTestCase):
         chan2 = PTM(ptm2, input_dims=2, output_dims=2)
         rho_targ = rho.evolve(chan1).evolve(chan2)
         self.assertEqual(rho.evolve(chan2.dot(chan1)), rho_targ)
+        self.assertEqual(rho.evolve(chan2 @ chan1), rho_targ)
 
     def test_compose_front(self):
         """Test deprecated front compose method."""
diff --git a/test/python/quantum_info/operators/channel/test_stinespring.py b/test/python/quantum_info/operators/channel/test_stinespring.py
index 334fa9863..d809009c2 100644
--- a/test/python/quantum_info/operators/channel/test_stinespring.py
+++ b/test/python/quantum_info/operators/channel/test_stinespring.py
@@ -197,11 +197,13 @@ class TestStinespring(ChannelTestCase):
         chan2 = Stinespring(self.UY)
         rho_targ = rho_init.evolve(Stinespring(self.UZ))
         self.assertEqual(rho_init.evolve(chan1.dot(chan2)), rho_targ)
+        self.assertEqual(rho_init.evolve(chan1 @ chan2), rho_targ)
 
         # 50% depolarizing channel
         chan1 = Stinespring(self.depol_stine(0.5))
         rho_targ = rho_init & Stinespring(self.depol_stine(0.75))
         self.assertEqual(rho_init.evolve(chan1.dot(chan1)), rho_targ)
+        self.assertEqual(rho_init.evolve(chan1 @ chan1), rho_targ)
 
         # Compose different dimensions
         stine1, stine2 = self.rand_matrix(16, 2), self.rand_matrix(8, 4)
@@ -209,6 +211,7 @@ class TestStinespring(ChannelTestCase):
         chan2 = Stinespring(stine2, input_dims=4, output_dims=2)
         rho_targ = rho_init & chan1 & chan2
         self.assertEqual(rho_init.evolve(chan2.dot(chan1)), rho_targ)
+        self.assertEqual(rho_init.evolve(chan2 @ chan1), rho_targ)
 
     def test_compose_front(self):
         """Test deprecated front compose method."""
diff --git a/test/python/quantum_info/operators/channel/test_superop.py b/test/python/quantum_info/operators/channel/test_superop.py
index ab47e5cb7..50297eaf7 100644
--- a/test/python/quantum_info/operators/channel/test_superop.py
+++ b/test/python/quantum_info/operators/channel/test_superop.py
@@ -265,11 +265,13 @@ class TestSuperOp(ChannelTestCase):
         chan2 = SuperOp(self.sopY)
         targ = SuperOp(self.sopZ)
         self.assertEqual(chan1.dot(chan2), targ)
+        self.assertEqual(chan1 @ chan2, targ)
 
         # 50% depolarizing channel
         chan1 = SuperOp(self.depol_sop(0.5))
         targ = SuperOp(self.depol_sop(0.75))
         self.assertEqual(chan1.dot(chan1), targ)
+        self.assertEqual(chan1 @ chan1, targ)
 
         # Random superoperator
         mat1 = self.rand_matrix(4, 4)
@@ -285,8 +287,12 @@ class TestSuperOp(ChannelTestCase):
         chan2 = SuperOp(self.rand_matrix(4, 16))
         chan = chan1.dot(chan2)
         self.assertEqual(chan.dim, (4, 4))
+        chan = chan1 @ chan2
+        self.assertEqual(chan.dim, (4, 4))
         chan = chan2.dot(chan1)
         self.assertEqual(chan.dim, (2, 2))
+        chan = chan2 @ chan1
+        self.assertEqual(chan.dim, (2, 2))
 
     def test_compose_front(self):
         """Test front compose method."""
diff --git a/test/python/quantum_info/operators/symplectic/test_pauli.py b/test/python/quantum_info/operators/symplectic/test_pauli.py
index e7aed73de..ba08b453c 100644
--- a/test/python/quantum_info/operators/symplectic/test_pauli.py
+++ b/test/python/quantum_info/operators/symplectic/test_pauli.py
@@ -258,6 +258,8 @@ class TestPauli(QiskitTestCase):
         op2 = operator_from_label(label2)
         target = op1.dot(op2)
         self.assertEqual(value, target)
+        target = op1 @ op2
+        self.assertEqual(value, target)
 
     @data(*pauli_group_labels(1))
     def test_dot_qargs(self, label2):
diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py
index 3780d44ef..81a9a0d10 100644
--- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py
+++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py
@@ -340,6 +340,11 @@ class TestSparsePauliOpMethods(QiskitTestCase):
         self.assertEqual(value, target)
         np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
 
+        op = spp_op1 @ spp_op2
+        value = op.to_operator()
+        self.assertEqual(value, target)
+        np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
+
     @combine(num_qubits=[1, 2, 3])
     def test_qargs_compose(self, num_qubits):
         """Test 3-qubit compose method with {num_qubits}-qubit qargs."""
diff --git a/test/python/quantum_info/operators/test_operator.py b/test/python/quantum_info/operators/test_operator.py
index 9ccc920f8..847e81638 100644
--- a/test/python/quantum_info/operators/test_operator.py
+++ b/test/python/quantum_info/operators/test_operator.py
@@ -336,9 +336,11 @@ class TestOperator(OperatorTestCase):
 
         targ = Operator(np.dot(self.UY, self.UX))
         self.assertEqual(op1.dot(op2), targ)
+        self.assertEqual(op1 @ op2, targ)
 
         targ = Operator(np.dot(self.UX, self.UY))
         self.assertEqual(op2.dot(op1), targ)
+        self.assertEqual(op2 @ op1, targ)
 
     def test_compose_front(self):
         """Test front compose method."""
diff --git a/test/python/quantum_info/operators/test_scalar_op.py b/test/python/quantum_info/operators/test_scalar_op.py
index bb11ddd0d..e6a2171b7 100644
--- a/test/python/quantum_info/operators/test_scalar_op.py
+++ b/test/python/quantum_info/operators/test_scalar_op.py
@@ -363,6 +363,10 @@ class TestScalarOpCompose(ScalarOpTestCase):
         target = coeff1 * coeff2
         self.assertScalarOp(val, dims, target)
 
+        val = op1 @ op2
+        target = coeff1 * coeff2
+        self.assertScalarOp(val, dims, target)
+
     @combine(coeff1=[0, 1, -3.1, 1 + 3j], coeff2=[0, -1, -5.1 - 2j])
     def test_matmul_scalar(self, coeff1, coeff2):
         """Test matmul method with two ScalarOp. ({coeff1}, {coeff2})"""