Repository: Qiskit/qiskit

Issue:
Initializer should use at most 2^(n+1)-2n cnots
<!-- ⚠️ 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?

Initializer should use at most 2^(n+1)-2n cnots (arXiv:quant-ph/0406176). 

Cost to initialize a 4 qubits state uses  
Number of cnots (optimization_level=0):  44
Number of cnots (optimization_level=3):  26
max number of cnots:  24

```python
num_qubits = 4
_optimization_level = 3

vector = -1 * np.random.rand(2 ** num_qubits)
vector[0] = 0
vector = vector / np.linalg.norm(vector)

qr = QuantumRegister(num_qubits, 'qr')
circuit = QuantumCircuit(qr)
circuit.initialize(vector, qr)

b = transpile(circuit, basis_gates=['u1', 'u2', 'u3', 'cx'], optimization_level=_optimization_level)
print('Number of cnots: ', b.count_ops()['cx'])
print('max number of cnots: ', 2 ** (num_qubits + 1) - 2 * num_qubits)
```



Discussion:

Resolving patch:
diff --git a/qiskit/extensions/quantum_initializer/initializer.py b/qiskit/extensions/quantum_initializer/initializer.py
--- a/qiskit/extensions/quantum_initializer/initializer.py
+++ b/qiskit/extensions/quantum_initializer/initializer.py
@@ -107,10 +107,19 @@ def gates_to_uncompute(self):
 
             # perform the required rotations to decouple the LSB qubit (so that
             # it can be "factored" out, leaving a shorter amplitude vector to peel away)
-            rz_mult = self._multiplex(RZGate, phis)
-            ry_mult = self._multiplex(RYGate, thetas)
-            circuit.append(rz_mult.to_instruction(), q[i:self.num_qubits])
-            circuit.append(ry_mult.to_instruction(), q[i:self.num_qubits])
+
+            add_last_cnot = True
+            if np.linalg.norm(phis) != 0 and np.linalg.norm(thetas) != 0:
+                add_last_cnot = False
+
+            if np.linalg.norm(phis) != 0:
+                rz_mult = self._multiplex(RZGate, phis, last_cnot=add_last_cnot)
+                circuit.append(rz_mult.to_instruction(), q[i:self.num_qubits])
+
+            if np.linalg.norm(thetas) != 0:
+                ry_mult = self._multiplex(RYGate, thetas, last_cnot=add_last_cnot)
+                circuit.append(ry_mult.to_instruction().mirror(), q[i:self.num_qubits])
+
         return circuit
 
     @staticmethod
@@ -178,7 +187,7 @@ def _bloch_angles(pair_of_complex):
 
         return final_r * np.exp(1.J * final_t / 2), theta, phi
 
-    def _multiplex(self, target_gate, list_of_angles):
+    def _multiplex(self, target_gate, list_of_angles, last_cnot=True):
         """
         Return a recursive implementation of a multiplexor circuit,
         where each instruction itself has a decomposition based on
@@ -190,6 +199,7 @@ def _multiplex(self, target_gate, list_of_angles):
             target_gate (Gate): Ry or Rz gate to apply to target qubit, multiplexed
                 over all other "select" qubits
             list_of_angles (list[float]): list of rotation angles to apply Ry and Rz
+            last_cnot (bool): add the last cnot if last_cnot = True
 
         Returns:
             DAGCircuit: the circuit implementing the multiplexor's action
@@ -217,7 +227,7 @@ def _multiplex(self, target_gate, list_of_angles):
         list_of_angles = angle_weight.dot(np.array(list_of_angles)).tolist()
 
         # recursive step on half the angles fulfilling the above assumption
-        multiplex_1 = self._multiplex(target_gate, list_of_angles[0:(list_len // 2)])
+        multiplex_1 = self._multiplex(target_gate, list_of_angles[0:(list_len // 2)], False)
         circuit.append(multiplex_1.to_instruction(), q[0:-1])
 
         # attach CNOT as follows, thereby flipping the LSB qubit
@@ -226,14 +236,15 @@ def _multiplex(self, target_gate, list_of_angles):
         # implement extra efficiency from the paper of cancelling adjacent
         # CNOTs (by leaving out last CNOT and reversing (NOT inverting) the
         # second lower-level multiplex)
-        multiplex_2 = self._multiplex(target_gate, list_of_angles[(list_len // 2):])
+        multiplex_2 = self._multiplex(target_gate, list_of_angles[(list_len // 2):], False)
         if list_len > 1:
             circuit.append(multiplex_2.to_instruction().mirror(), q[0:-1])
         else:
             circuit.append(multiplex_2.to_instruction(), q[0:-1])
 
         # attach a final CNOT
-        circuit.append(CXGate(), [msb, lsb])
+        if last_cnot:
+            circuit.append(CXGate(), [msb, lsb])
 
         return circuit
 


Test patch:
diff --git a/test/python/circuit/test_initializer.py b/test/python/circuit/test_initializer.py
--- a/test/python/circuit/test_initializer.py
+++ b/test/python/circuit/test_initializer.py
@@ -23,6 +23,7 @@
 from qiskit import QuantumCircuit
 from qiskit import QuantumRegister
 from qiskit import ClassicalRegister
+from qiskit import transpile
 from qiskit import execute, assemble, BasicAer
 from qiskit.quantum_info import state_fidelity
 from qiskit.exceptions import QiskitError
@@ -314,6 +315,35 @@ def test_equivalence(self):
 
         self.assertEqual(qc1, qc2)
 
+    def test_max_number_cnots(self):
+        """
+        Check if the number of cnots <= 2^(n+1) - 2n (arXiv:quant-ph/0406176)
+        """
+        num_qubits = 4
+        _optimization_level = 0
+
+        vector = np.array(
+            [0.1314346 + 0.j, 0.32078572 - 0.01542775j, 0.13146466 + 0.0945312j,
+             0.21090852 + 0.07935982j, 0.1700122 - 0.07905648j, 0.15570757 - 0.12309154j,
+             0.18039667 + 0.04904504j, 0.22227187 - 0.05055569j, 0.23573255 - 0.09894111j,
+             0.27307292 - 0.10372994j, 0.24162792 + 0.1090791j, 0.3115577 + 0.1211683j,
+             0.1851788 + 0.08679141j, 0.36226463 - 0.09940202j, 0.13863395 + 0.10558225j,
+             0.30767986 + 0.02073838j])
+
+        vector = vector / np.linalg.norm(vector)
+
+        qr = QuantumRegister(num_qubits, 'qr')
+        circuit = QuantumCircuit(qr)
+        circuit.initialize(vector, qr)
+
+        b = transpile(circuit, basis_gates=['u1', 'u2', 'u3', 'cx'],
+                      optimization_level=_optimization_level)
+
+        number_cnots = b.count_ops()['cx']
+        max_cnots = 2 ** (num_qubits + 1) - 2 * num_qubits
+
+        self.assertLessEqual(number_cnots, max_cnots)
+
 
 class TestInstructionParam(QiskitTestCase):
     """Test conversion of numpy type parameters."""