Repository: Qiskit/qiskit

Issue:
Add `&` and `^` overloads for QuantumCircuit.compose and .tensor
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?

Following #5834 and #4208, `QuantumCircuit` should have similar shorthand for `.compose` and `.tensor`, following quantum_info. Should be released in 0.17 so users can transition directly from `+`/`+=`.



Discussion:

Resolving patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -767,7 +767,7 @@ def compose(self, other, qubits=None, clbits=None, front=False, inplace=False):
 
         return dest
 
-    def tensor(self, other):
+    def tensor(self, other, inplace=False):
         """Tensor ``self`` with ``other``.
 
         Remember that in the little-endian convention the leftmost operation will be at the bottom
@@ -785,6 +785,7 @@ def tensor(self, other):
 
         Args:
             other (QuantumCircuit): The other circuit to tensor this circuit with.
+            inplace (bool): If True, modify the object. Otherwise return composed circuit.
 
         Examples:
 
@@ -832,6 +833,11 @@ def tensor(self, other):
         dest.compose(other, range(other.num_qubits), range(other.num_clbits), inplace=True)
         dest.compose(self, range(other.num_qubits, num_qubits),
                      range(other.num_clbits, num_clbits), inplace=True)
+
+        # Replace information from tensored circuit into self when inplace = True
+        if inplace:
+            self.__dict__.update(dest.__dict__)
+            return None
         return dest
 
     @property
@@ -869,6 +875,24 @@ def __iadd__(self, rhs):
         """Overload += to implement self.extend."""
         return self.extend(rhs)
 
+    def __and__(self, rhs):
+        """Overload & to implement self.compose."""
+        return self.compose(rhs)
+
+    def __iand__(self, rhs):
+        """Overload &= to implement self.compose in place."""
+        self.compose(rhs, inplace=True)
+        return self
+
+    def __xor__(self, top):
+        """Overload ^ to implement self.tensor."""
+        return self.tensor(top)
+
+    def __ixor__(self, top):
+        """Overload ^= to implement self.tensor in place."""
+        self.tensor(top, inplace=True)
+        return self
+
     def __len__(self):
         """Return number of operations in circuit."""
         return len(self._data)


Test patch:
diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py
--- a/test/python/circuit/test_circuit_operations.py
+++ b/test/python/circuit/test_circuit_operations.py
@@ -175,6 +175,169 @@ def test_extend_circuit_adds_qubits(self):
 
         self.assertListEqual(empty.qubits, qr[:])
 
+    def test_compose_circuit(self):
+        """Test composing two circuits"""
+        qr = QuantumRegister(2)
+        cr = ClassicalRegister(2)
+        qc1 = QuantumCircuit(qr, cr)
+        qc2 = QuantumCircuit(qr, cr)
+        qc1.h(qr[0])
+        qc1.measure(qr[0], cr[0])
+        qc2.measure(qr[1], cr[1])
+
+        qc3 = qc1.compose(qc2)
+        backend = BasicAer.get_backend('qasm_simulator')
+        shots = 1024
+        result = execute(qc3, backend=backend, shots=shots,
+                         seed_simulator=78).result()
+        counts = result.get_counts()
+        target = {'00': shots / 2, '01': shots / 2}
+        threshold = 0.04 * shots
+        self.assertDictEqual(qc3.count_ops(), {'h': 1, 'measure': 2})
+        self.assertDictEqual(qc1.count_ops(), {'h': 1, 'measure': 1})  # no changes "in-place"
+        self.assertDictEqual(qc2.count_ops(), {'measure': 1})  # no changes "in-place"
+        self.assertDictAlmostEqual(counts, target, threshold)
+
+    def test_compose_circuit_and(self):
+        """Test composing two circuits using & operator"""
+        qr = QuantumRegister(2)
+        cr = ClassicalRegister(2)
+        qc1 = QuantumCircuit(qr, cr)
+        qc2 = QuantumCircuit(qr, cr)
+        qc1.h(qr[0])
+        qc1.measure(qr[0], cr[0])
+        qc2.measure(qr[1], cr[1])
+
+        qc3 = qc1 & qc2
+        backend = BasicAer.get_backend('qasm_simulator')
+        shots = 1024
+        result = execute(qc3, backend=backend, shots=shots,
+                         seed_simulator=78).result()
+        counts = result.get_counts()
+        target = {'00': shots / 2, '01': shots / 2}
+        threshold = 0.04 * shots
+        self.assertDictEqual(qc3.count_ops(), {'h': 1, 'measure': 2})
+        self.assertDictEqual(qc1.count_ops(), {'h': 1, 'measure': 1})  # no changes "in-place"
+        self.assertDictEqual(qc2.count_ops(), {'measure': 1})  # no changes "in-place"
+        self.assertDictAlmostEqual(counts, target, threshold)
+
+    def test_compose_circuit_iand(self):
+        """Test composing circuits using &= operator (in place)"""
+        qr = QuantumRegister(2)
+        cr = ClassicalRegister(2)
+        qc1 = QuantumCircuit(qr, cr)
+        qc2 = QuantumCircuit(qr, cr)
+        qc1.h(qr[0])
+        qc1.measure(qr[0], cr[0])
+        qc2.measure(qr[1], cr[1])
+
+        qc1 &= qc2
+        backend = BasicAer.get_backend('qasm_simulator')
+        shots = 1024
+        result = execute(qc1, backend=backend, shots=shots,
+                         seed_simulator=78).result()
+        counts = result.get_counts()
+        target = {'00': shots / 2, '01': shots / 2}
+        threshold = 0.04 * shots
+        self.assertDictEqual(qc1.count_ops(), {'h': 1, 'measure': 2})  # changes "in-place"
+        self.assertDictEqual(qc2.count_ops(), {'measure': 1})  # no changes "in-place"
+        self.assertDictAlmostEqual(counts, target, threshold)
+
+    def test_compose_circuit_fail_circ_size(self):
+        """Test composing circuit fails when number of wires in circuit is not enough"""
+        qr1 = QuantumRegister(2)
+        qr2 = QuantumRegister(4)
+
+        # Creating our circuits
+        qc1 = QuantumCircuit(qr1)
+        qc1.x(0)
+        qc1.h(1)
+
+        qc2 = QuantumCircuit(qr2)
+        qc2.h([1, 2])
+        qc2.cx(2, 3)
+
+        # Composing will fail because qc2 requires 4 wires
+        self.assertRaises(CircuitError, qc1.compose, qc2)
+
+    def test_compose_circuit_fail_arg_size(self):
+        """Test composing circuit fails when arg size does not match number of wires"""
+        qr1 = QuantumRegister(2)
+        qr2 = QuantumRegister(2)
+
+        qc1 = QuantumCircuit(qr1)
+        qc1.h(0)
+
+        qc2 = QuantumCircuit(qr2)
+        qc2.cx(0, 1)
+
+        self.assertRaises(CircuitError, qc1.compose, qc2, qubits=[0])
+
+    def test_tensor_circuit(self):
+        """Test tensoring two circuits"""
+        qc1 = QuantumCircuit(1, 1)
+        qc2 = QuantumCircuit(1, 1)
+
+        qc2.h(0)
+        qc2.measure(0, 0)
+        qc1.measure(0, 0)
+
+        qc3 = qc1.tensor(qc2)
+        backend = BasicAer.get_backend('qasm_simulator')
+        shots = 1024
+        result = execute(qc3, backend=backend, shots=shots,
+                         seed_simulator=78).result()
+        counts = result.get_counts()
+        target = {'00': shots / 2, '01': shots / 2}
+        threshold = 0.04 * shots
+        self.assertDictEqual(qc3.count_ops(), {'h': 1, 'measure': 2})
+        self.assertDictEqual(qc2.count_ops(), {'h': 1, 'measure': 1})  # no changes "in-place"
+        self.assertDictEqual(qc1.count_ops(), {'measure': 1})  # no changes "in-place"
+        self.assertDictAlmostEqual(counts, target, threshold)
+
+    def test_tensor_circuit_xor(self):
+        """Test tensoring two circuits using ^ operator"""
+        qc1 = QuantumCircuit(1, 1)
+        qc2 = QuantumCircuit(1, 1)
+
+        qc2.h(0)
+        qc2.measure(0, 0)
+        qc1.measure(0, 0)
+
+        qc3 = qc1 ^ qc2
+        backend = BasicAer.get_backend('qasm_simulator')
+        shots = 1024
+        result = execute(qc3, backend=backend, shots=shots,
+                         seed_simulator=78).result()
+        counts = result.get_counts()
+        target = {'00': shots / 2, '01': shots / 2}
+        threshold = 0.04 * shots
+        self.assertDictEqual(qc3.count_ops(), {'h': 1, 'measure': 2})
+        self.assertDictEqual(qc2.count_ops(), {'h': 1, 'measure': 1})  # no changes "in-place"
+        self.assertDictEqual(qc1.count_ops(), {'measure': 1})  # no changes "in-place"
+        self.assertDictAlmostEqual(counts, target, threshold)
+
+    def test_tensor_circuit_ixor(self):
+        """Test tensoring two circuits using ^= operator"""
+        qc1 = QuantumCircuit(1, 1)
+        qc2 = QuantumCircuit(1, 1)
+
+        qc2.h(0)
+        qc2.measure(0, 0)
+        qc1.measure(0, 0)
+
+        qc1 ^= qc2
+        backend = BasicAer.get_backend('qasm_simulator')
+        shots = 1024
+        result = execute(qc1, backend=backend, shots=shots,
+                         seed_simulator=78).result()
+        counts = result.get_counts()
+        target = {'00': shots / 2, '01': shots / 2}
+        threshold = 0.04 * shots
+        self.assertDictEqual(qc1.count_ops(), {'h': 1, 'measure': 2})  # changes "in-place"
+        self.assertDictEqual(qc2.count_ops(), {'h': 1, 'measure': 1})  # no changes "in-place"
+        self.assertDictAlmostEqual(counts, target, threshold)
+
     def test_measure_args_type_cohesion(self):
         """Test for proper args types for measure function.
         """