Repository: Qiskit/qiskit

Issue:
BasicAer qasm_simulator missing 'unitary' implementation
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**: 0.8
- **Python version**: 3.6
- **Operating system**: MacOS 10.14

### What is the current behavior?

The BasicAer qasm simulator has 'unitary' in its bases gates, but does not have an implementation for arbitrary unitaries leading to an exception for any circuit that contains them.

### Steps to reproduce the problem

```python
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
from qiskit import execute, BasicAer
from qiskit.quantum_info.operators import Operator

# Add to a circuit
qr = QuantumRegister(1)
cr = ClassicalRegister(1)
circ = QuantumCircuit(qr, cr)
circ.append(Operator([[0, 1], [1, 0]]), [qr[0]])
circ.measure(qr, cr)

backend = BasicAer.get_backend('qasm_simulator')
job = execute(circ, backend)
job.result().get_counts(0)
```

Raises
`BasicAerError: 'qasm_simulator encountered unrecognized operation "unitary"'`

### What is the expected behavior?

Circuit executes

### Suggested solutions

I will put in a PR to add an implementation of arbitrary unitaries to the BasicAer simulators.



Discussion:

Resolving patch:
diff --git a/qiskit/providers/basicaer/qasm_simulator.py b/qiskit/providers/basicaer/qasm_simulator.py
--- a/qiskit/providers/basicaer/qasm_simulator.py
+++ b/qiskit/providers/basicaer/qasm_simulator.py
@@ -133,40 +133,23 @@ def __init__(self, configuration=None, provider=None):
         # TEMP
         self._sample_measure = False
 
-    def _add_unitary_single(self, gate, qubit):
-        """Apply an arbitrary 1-qubit unitary matrix.
+    def _add_unitary(self, gate, qubits):
+        """Apply an N-qubit unitary matrix.
 
         Args:
-            gate (matrix_like): a single qubit gate matrix
-            qubit (int): the qubit to apply gate to
+            gate (matrix_like): an N-qubit unitary matrix
+            qubits (list): the list of N-qubits.
         """
+        # Get the number of qubits
+        num_qubits = len(qubits)
         # Compute einsum index string for 1-qubit matrix multiplication
-        indexes = einsum_vecmul_index([qubit], self._number_of_qubits)
-        # Convert to complex rank-2 tensor
-        gate_tensor = np.array(gate, dtype=complex)
+        indexes = einsum_vecmul_index(qubits, self._number_of_qubits)
+        # Convert to complex rank-2N tensor
+        gate_tensor = np.reshape(np.array(gate, dtype=complex),
+                                 num_qubits * [2, 2])
         # Apply matrix multiplication
-        self._statevector = np.einsum(indexes, gate_tensor,
-                                      self._statevector,
-                                      dtype=complex,
-                                      casting='no')
-
-    def _add_unitary_two(self, gate, qubit0, qubit1):
-        """Apply a two-qubit unitary matrix.
-
-        Args:
-            gate (matrix_like): a the two-qubit gate matrix
-            qubit0 (int): gate qubit-0
-            qubit1 (int): gate qubit-1
-        """
-        # Compute einsum index string for 1-qubit matrix multiplication
-        indexes = einsum_vecmul_index([qubit0, qubit1], self._number_of_qubits)
-        # Convert to complex rank-4 tensor
-        gate_tensor = np.reshape(np.array(gate, dtype=complex), 4 * [2])
-        # Apply matrix multiplication
-        self._statevector = np.einsum(indexes, gate_tensor,
-                                      self._statevector,
-                                      dtype=complex,
-                                      casting='no')
+        self._statevector = np.einsum(indexes, gate_tensor, self._statevector,
+                                      dtype=complex, casting='no')
 
     def _get_measure_outcome(self, qubit):
         """Simulate the outcome of measurement of a qubit.
@@ -252,7 +235,7 @@ def _add_qasm_measure(self, qubit, cmembit, cregbit=None):
         else:
             update_diag = [[0, 0], [0, 1 / np.sqrt(probability)]]
         # update classical state
-        self._add_unitary_single(update_diag, qubit)
+        self._add_unitary(update_diag, [qubit])
 
     def _add_qasm_reset(self, qubit):
         """Apply a reset instruction to a qubit.
@@ -269,10 +252,10 @@ def _add_qasm_reset(self, qubit):
         # update quantum state
         if outcome == '0':
             update = [[1 / np.sqrt(probability), 0], [0, 0]]
-            self._add_unitary_single(update, qubit)
+            self._add_unitary(update, [qubit])
         else:
             update = [[0, 1 / np.sqrt(probability)], [0, 0]]
-            self._add_unitary_single(update, qubit)
+            self._add_unitary(update, [qubit])
 
     def _validate_initial_statevector(self):
         """Validate an initial statevector"""
@@ -521,11 +504,15 @@ def run_experiment(self, experiment):
                             continue
 
                 # Check if single  gate
-                if operation.name in ('U', 'u1', 'u2', 'u3'):
+                if operation.name == 'unitary':
+                    qubits = operation.qubits
+                    gate = operation.params[0]
+                    self._add_unitary(gate, qubits)
+                elif operation.name in ('U', 'u1', 'u2', 'u3'):
                     params = getattr(operation, 'params', None)
                     qubit = operation.qubits[0]
                     gate = single_gate_matrix(operation.name, params)
-                    self._add_unitary_single(gate, qubit)
+                    self._add_unitary(gate, [qubit])
                 # Check if CX gate
                 elif operation.name in ('id', 'u0'):
                     pass
@@ -533,7 +520,7 @@ def run_experiment(self, experiment):
                     qubit0 = operation.qubits[0]
                     qubit1 = operation.qubits[1]
                     gate = cx_gate_matrix()
-                    self._add_unitary_two(gate, qubit0, qubit1)
+                    self._add_unitary(gate, [qubit0, qubit1])
                 # Check if reset
                 elif operation.name == 'reset':
                     qubit = operation.qubits[0]
diff --git a/qiskit/providers/basicaer/statevector_simulator.py b/qiskit/providers/basicaer/statevector_simulator.py
--- a/qiskit/providers/basicaer/statevector_simulator.py
+++ b/qiskit/providers/basicaer/statevector_simulator.py
@@ -53,7 +53,7 @@ class StatevectorSimulatorPy(QasmSimulatorPy):
         'max_shots': 65536,
         'coupling_map': None,
         'description': 'A Python statevector simulator for qobj files',
-        'basis_gates': ['u1', 'u2', 'u3', 'cx', 'id', 'snapshot'],
+        'basis_gates': ['u1', 'u2', 'u3', 'cx', 'id', 'unitary'],
         'gates': [
             {
                 'name': 'u1',
@@ -81,9 +81,9 @@ class StatevectorSimulatorPy(QasmSimulatorPy):
                 'qasm_def': 'gate id a { U(0,0,0) a; }'
             },
             {
-                'name': 'snapshot',
-                'parameters': ['slot'],
-                'qasm_def': 'gate snapshot(slot) q { TODO }'
+                'name': 'unitary',
+                'parameters': ['matrix'],
+                'qasm_def': 'unitary(matrix) q1, q2,...'
             }
         ]
     }
diff --git a/qiskit/providers/basicaer/unitary_simulator.py b/qiskit/providers/basicaer/unitary_simulator.py
--- a/qiskit/providers/basicaer/unitary_simulator.py
+++ b/qiskit/providers/basicaer/unitary_simulator.py
@@ -68,7 +68,7 @@ class UnitarySimulatorPy(BaseBackend):
         'max_shots': 65536,
         'coupling_map': None,
         'description': 'A python simulator for unitary matrix corresponding to a circuit',
-        'basis_gates': ['u1', 'u2', 'u3', 'cx', 'id'],
+        'basis_gates': ['u1', 'u2', 'u3', 'cx', 'id', 'unitary'],
         'gates': [
             {
                 'name': 'u1',
@@ -94,6 +94,11 @@ class UnitarySimulatorPy(BaseBackend):
                 'name': 'id',
                 'parameters': ['a'],
                 'qasm_def': 'gate id a { U(0,0,0) a; }'
+            },
+            {
+                'name': 'unitary',
+                'parameters': ['matrix'],
+                'qasm_def': 'unitary(matrix) q1, q2,...'
             }
         ]
     }
@@ -114,36 +119,20 @@ def __init__(self, configuration=None, provider=None):
         self._initial_unitary = None
         self._chop_threshold = 1e-15
 
-    def _add_unitary_single(self, gate, qubit):
-        """Apply an arbitrary 1-qubit unitary matrix.
+    def _add_unitary(self, gate, qubits):
+        """Apply an N-qubit unitary matrix.
 
         Args:
-            gate (matrix_like): a single qubit gate matrix
-            qubit (int): the qubit to apply gate to
+            gate (matrix_like): an N-qubit unitary matrix
+            qubits (list): the list of N-qubits.
         """
-        # Convert to complex rank-2 tensor
-        gate_tensor = np.array(gate, dtype=complex)
+        # Get the number of qubits
+        num_qubits = len(qubits)
         # Compute einsum index string for 1-qubit matrix multiplication
-        indexes = einsum_matmul_index([qubit], self._number_of_qubits)
-        # Apply matrix multiplication
-        self._unitary = np.einsum(indexes, gate_tensor, self._unitary,
-                                  dtype=complex, casting='no')
-
-    def _add_unitary_two(self, gate, qubit0, qubit1):
-        """Apply a two-qubit unitary matrix.
-
-        Args:
-            gate (matrix_like): a the two-qubit gate matrix
-            qubit0 (int): gate qubit-0
-            qubit1 (int): gate qubit-1
-        """
-
-        # Convert to complex rank-4 tensor
-        gate_tensor = np.reshape(np.array(gate, dtype=complex), 4 * [2])
-
-        # Compute einsum index string for 2-qubit matrix multiplication
-        indexes = einsum_matmul_index([qubit0, qubit1], self._number_of_qubits)
-
+        indexes = einsum_matmul_index(qubits, self._number_of_qubits)
+        # Convert to complex rank-2N tensor
+        gate_tensor = np.reshape(np.array(gate, dtype=complex),
+                                 num_qubits * [2, 2])
         # Apply matrix multiplication
         self._unitary = np.einsum(indexes, gate_tensor, self._unitary,
                                   dtype=complex, casting='no')
@@ -324,12 +313,16 @@ def run_experiment(self, experiment):
         self._initialize_unitary()
 
         for operation in experiment.instructions:
+            if operation.name == 'unitary':
+                qubits = operation.qubits
+                gate = operation.params[0]
+                self._add_unitary(gate, qubits)
             # Check if single  gate
-            if operation.name in ('U', 'u1', 'u2', 'u3'):
+            elif operation.name in ('U', 'u1', 'u2', 'u3'):
                 params = getattr(operation, 'params', None)
                 qubit = operation.qubits[0]
                 gate = single_gate_matrix(operation.name, params)
-                self._add_unitary_single(gate, qubit)
+                self._add_unitary(gate, [qubit])
             elif operation.name in ('id', 'u0'):
                 pass
             # Check if CX gate
@@ -337,7 +330,7 @@ def run_experiment(self, experiment):
                 qubit0 = operation.qubits[0]
                 qubit1 = operation.qubits[1]
                 gate = cx_gate_matrix()
-                self._add_unitary_two(gate, qubit0, qubit1)
+                self._add_unitary(gate, [qubit0, qubit1])
             # Check if barrier
             elif operation.name == 'barrier':
                 pass


Test patch:
diff --git a/test/python/basicaer/test_qasm_simulator.py b/test/python/basicaer/test_qasm_simulator.py
--- a/test/python/basicaer/test_qasm_simulator.py
+++ b/test/python/basicaer/test_qasm_simulator.py
@@ -160,6 +160,31 @@ def test_memory(self):
         for mem in memory:
             self.assertIn(mem, ['10 00', '10 11'])
 
+    def test_unitary(self):
+        """Test unitary gate instruction"""
+        max_qubits = 4
+        x_mat = np.array([[0, 1], [1, 0]])
+        # Test 1 to max_qubits for random n-qubit unitary gate
+        for i in range(max_qubits):
+            num_qubits = i + 1
+            # Apply X gate to all qubits
+            multi_x = x_mat
+            for _ in range(i):
+                multi_x = np.kron(multi_x, x_mat)
+            # Target counts
+            shots = 100
+            target_counts = {num_qubits * '1': shots}
+            # Test circuit
+            qr = QuantumRegister(num_qubits, 'qr')
+            cr = ClassicalRegister(num_qubits, 'cr')
+            circuit = QuantumCircuit(qr, cr)
+            circuit.unitary(multi_x, qr)
+            circuit.measure(qr, cr)
+            job = execute(circuit, self.backend, shots=shots)
+            result = job.result()
+            counts = result.get_counts(0)
+            self.assertEqual(counts, target_counts)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/test/python/basicaer/test_statevector_simulator.py b/test/python/basicaer/test_statevector_simulator.py
--- a/test/python/basicaer/test_statevector_simulator.py
+++ b/test/python/basicaer/test_statevector_simulator.py
@@ -20,6 +20,9 @@
 from qiskit.providers.basicaer import StatevectorSimulatorPy
 from qiskit.test import ReferenceCircuits
 from qiskit.test import providers
+from qiskit import QuantumRegister, QuantumCircuit, execute
+from qiskit.quantum_info.random import random_unitary
+from qiskit.quantum_info import state_fidelity
 
 
 class StatevectorSimulatorTest(providers.BackendTestCase):
@@ -58,6 +61,30 @@ def test_measure_collapse(self):
         # state is 1/sqrt(2)|00> + 1/sqrt(2)|11>, up to a global phase
         self.assertTrue(success)
 
+    def test_unitary(self):
+        """Test unitary gate instruction"""
+        num_trials = 10
+        max_qubits = 3
+        # Test 1 to max_qubits for random n-qubit unitary gate
+        for i in range(max_qubits):
+            num_qubits = i + 1
+            psi_init = np.zeros(2 ** num_qubits)
+            psi_init[0] = 1.0
+            qr = QuantumRegister(num_qubits, 'qr')
+            for _ in range(num_trials):
+                # Create random unitary
+                unitary = random_unitary(2 ** num_qubits)
+                # Compute expected output state
+                psi_target = unitary.data.dot(psi_init)
+                # Simulate output on circuit
+                circuit = QuantumCircuit(qr)
+                circuit.unitary(unitary, qr)
+                job = execute(circuit, self.backend)
+                result = job.result()
+                psi_out = result.get_statevector(0)
+                fidelity = state_fidelity(psi_target, psi_out)
+                self.assertGreater(fidelity, 0.999)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/test/python/basicaer/test_unitary_simulator.py b/test/python/basicaer/test_unitary_simulator.py
--- a/test/python/basicaer/test_unitary_simulator.py
+++ b/test/python/basicaer/test_unitary_simulator.py
@@ -23,6 +23,8 @@
 from qiskit.providers.basicaer import UnitarySimulatorPy
 from qiskit.test import ReferenceCircuits
 from qiskit.test import providers
+from qiskit.quantum_info.random import random_unitary
+from qiskit.quantum_info import process_fidelity
 
 
 class BasicAerUnitarySimulatorPyTest(providers.BackendTestCase):
@@ -95,6 +97,29 @@ def _reference_unitaries(self):
         return [target_unitary1, target_unitary2, target_unitary3,
                 target_unitary4, target_unitary5]
 
+    def test_unitary(self):
+        """Test unitary gate instruction"""
+        num_trials = 10
+        max_qubits = 3
+        # Test 1 to max_qubits for random n-qubit unitary gate
+        for i in range(max_qubits):
+            num_qubits = i + 1
+            unitary_init = np.eye(2 ** num_qubits)
+            qr = QuantumRegister(num_qubits, 'qr')
+            for _ in range(num_trials):
+                # Create random unitary
+                unitary = random_unitary(2 ** num_qubits)
+                # Compute expected output state
+                unitary_target = unitary.data.dot(unitary_init)
+                # Simulate output on circuit
+                circuit = QuantumCircuit(qr)
+                circuit.unitary(unitary, qr)
+                job = execute(circuit, self.backend)
+                result = job.result()
+                unitary_out = result.get_unitary(0)
+                fidelity = process_fidelity(unitary_target, unitary_out)
+                self.assertGreater(fidelity, 0.999)
+
 
 if __name__ == '__main__':
     unittest.main()