Repository: Qiskit/qiskit

Issue:
Unnecessary performance bottleneck in Statevector.evolve
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Informations

- **Qiskit Aer version**: 0.5.0
- **Python version**: 3.8
- **Operating system**: linux (ubuntu 18.04)

### What is the current behavior?

Statevector.evolve is slow, because it recreates Statevector objects, which itself is a slow operation.

### Steps to reproduce the problem

Throw a line profiler on a code segment that calls Statevector.evolve repeatedly.

### What is the expected behavior?

Creating a Statevector should have little to no overhead.

### Suggested solutions

I profiled some code that uses Statevector.evolve repeatedly, and identified the bottleneck:

```
# qiskit/quantum_info/states/statevector.py in Statevector.evolve
return Statevector(np.reshape(tensor, np.product(new_dims)), dims=new_dims)
# same file, Statevector.__init__
super().__init__(...)
# qiskit/quantum_info/states/quantum_state.py in QuantumState.__init__
self._rng = np.random.RandomState()
```
Now, creating a RandomState object seems to have tremendous overhead; indeed, the np.reshape operation of tensor is much quicker.

Workaround: after loading qiskit and numpy, run
```
def _global_rng_fix():
    return _global_rng_fix._random_state
_global_rng_fix._random_state = np.random.RandomState()
np.random.RandomState = _global_rng_fix
```

This is not a fix! A true fix would be to use a newer rng object that doesn't have massive overhead (note that RandomState is legacy code anyhow - and since there's no seed set, it's unclear what the benefit of a separate RandomState object should be within a Statevector anyhow).

Thanks for your great work!
~ Johannes



Discussion:

Resolving patch:
diff --git a/qiskit/quantum_info/states/densitymatrix.py b/qiskit/quantum_info/states/densitymatrix.py
--- a/qiskit/quantum_info/states/densitymatrix.py
+++ b/qiskit/quantum_info/states/densitymatrix.py
@@ -38,42 +38,39 @@ class DensityMatrix(QuantumState):
 
     def __init__(self, data, dims=None):
         """Initialize a state object."""
-        if isinstance(data, Statevector):
-            # We convert a statevector into a density matrix by taking the projector
-            state_vec = data.data
-            mat = np.outer(state_vec, np.conjugate(state_vec))
-            if dims is None:
-                dims = data.dims()
+        if isinstance(data, (list, np.ndarray)):
+            # Finally we check if the input is a raw matrix in either a
+            # python list or numpy array format.
+            self._data = np.asarray(data, dtype=complex)
         elif hasattr(data, 'to_operator'):
             # If the data object has a 'to_operator' attribute this is given
             # higher preference than the 'to_matrix' method for initializing
             # an Operator object.
-            data = data.to_operator()
-            mat = data.data
+            op = data.to_operator()
+            self._data = op.data
             if dims is None:
-                dims = data.output_dims()
+                dims = op._output_dims
         elif hasattr(data, 'to_matrix'):
             # If no 'to_operator' attribute exists we next look for a
             # 'to_matrix' attribute to a matrix that will be cast into
             # a complex numpy matrix.
-            mat = np.asarray(data.to_matrix(), dtype=complex)
-        elif isinstance(data, (list, np.ndarray)):
-            # Finally we check if the input is a raw matrix in either a
-            # python list or numpy array format.
-            mat = np.asarray(data, dtype=complex)
+            self._data = np.asarray(data.to_matrix(), dtype=complex)
         else:
             raise QiskitError("Invalid input data format for DensityMatrix")
         # Convert statevector into a density matrix
-        if mat.ndim == 2 and mat.shape[1] == 1:
-            mat = np.reshape(mat, mat.shape[0])
-        if mat.ndim == 1:
-            mat = np.outer(mat, np.conj(mat))
-        # Determine input and output dimensions
-        if mat.ndim != 2 or mat.shape[0] != mat.shape[1]:
+        ndim = self._data.ndim
+        shape = self._data.shape
+        if ndim == 2 and shape[0] == shape[1]:
+            pass  # We good
+        elif ndim == 1:
+            self._data = np.outer(self._data, np.conj(self._data))
+        elif ndim == 2 and shape[1] == 1:
+            self._data = np.reshape(self._data, shape[0])
+            shape = self._data.shape
+        else:
             raise QiskitError(
                 "Invalid DensityMatrix input: not a square matrix.")
-        self._data = mat
-        super().__init__(self._automatic_dims(dims, self._data.shape[0]))
+        super().__init__(self._automatic_dims(dims, shape[0]))
 
     def __eq__(self, other):
         return super().__eq__(other) and np.allclose(
@@ -224,11 +221,13 @@ def evolve(self, other, qargs=None):
 
         # Evolution by a QuantumChannel
         if hasattr(other, 'to_quantumchannel'):
-            other = other.to_quantumchannel()
+            return other.to_quantumchannel()._evolve(self, qargs=qargs)
         if isinstance(other, QuantumChannel):
             return other._evolve(self, qargs=qargs)
 
         # Unitary evolution by an Operator
+        if not isinstance(other, Operator):
+            other = Operator(other)
         return self._evolve_operator(other, qargs=qargs)
 
     def probabilities(self, qargs=None, decimals=None):
@@ -383,7 +382,7 @@ def from_instruction(cls, instruction):
         num_qubits = instruction.num_qubits
         init = np.zeros((2**num_qubits, 2**num_qubits), dtype=complex)
         init[0, 0] = 1
-        vec = DensityMatrix(init, dims=num_qubits * [2])
+        vec = DensityMatrix(init, dims=num_qubits * (2, ))
         vec._append_instruction(instruction)
         return vec
 
@@ -458,16 +457,15 @@ def _shape(self):
 
     def _evolve_operator(self, other, qargs=None):
         """Evolve density matrix by an operator"""
-        if not isinstance(other, Operator):
-            other = Operator(other)
         if qargs is None:
             # Evolution on full matrix
             if self._dim != other._input_dim:
                 raise QiskitError(
                     "Operator input dimension is not equal to density matrix dimension."
                 )
-            mat = np.dot(other.data, self.data).dot(other.adjoint().data)
-            return DensityMatrix(mat, dims=other.output_dims())
+            op_mat = other.data
+            mat = np.dot(op_mat, self.data).dot(op_mat.T.conj())
+            return DensityMatrix(mat, dims=other._output_dims)
         # Otherwise we are applying an operator only to subsystems
         # Check dimensions of subsystems match the operator
         if self.dims(qargs) != other.input_dims():
@@ -531,7 +529,7 @@ def _evolve_instruction(self, obj, qargs=None):
         """Return a new statevector by applying an instruction."""
         if isinstance(obj, QuantumCircuit):
             obj = obj.to_instruction()
-        vec = DensityMatrix(self.data, dims=self.dims())
+        vec = DensityMatrix(self.data, dims=self._dims)
         vec._append_instruction(obj, qargs=qargs)
         return vec
 
diff --git a/qiskit/quantum_info/states/quantum_state.py b/qiskit/quantum_info/states/quantum_state.py
--- a/qiskit/quantum_info/states/quantum_state.py
+++ b/qiskit/quantum_info/states/quantum_state.py
@@ -45,7 +45,7 @@ def __init__(self, dims):
         self._num_qubits = None  # number of qubit subsystems if N-qubit state
         self._set_dims(dims)
         # RNG for measure functions
-        self._rng = np.random.RandomState()
+        self._rng_generator = None
 
     def __eq__(self, other):
         return isinstance(other, self.__class__) and self.dims() == other.dims()
@@ -94,6 +94,12 @@ def set_rtol(cls, value):
                     value, cls._MAX_TOL))
         cls._RTOL_DEFAULT = value
 
+    @property
+    def _rng(self):
+        if self._rng_generator is None:
+            return np.random
+        return self._rng_generator
+
     def _reshape(self, dims=None):
         """Reshape dimensions of the state.
 
@@ -126,7 +132,12 @@ def copy(self):
 
     def seed(self, value=None):
         """Set the seed for the quantum state RNG."""
-        self._rng.seed(value)
+        if value is None:
+            self._rng_generator = None
+        elif isinstance(value, np.random.RandomState):
+            self._rng_generator = value
+        else:
+            self._rng_generator = np.random.RandomState(value)
 
     @abstractmethod
     def is_valid(self, atol=None, rtol=None):
@@ -360,8 +371,6 @@ def sample_memory(self, shots, qargs=None):
         # Generate list of possible outcome string labels
         labels = self._index_to_ket_array(
             np.arange(len(probs)), self.dims(qargs), string_labels=True)
-
-        # Sample outcomes
         return self._rng.choice(labels, p=probs, size=shots)
 
     def sample_counts(self, shots, qargs=None):
diff --git a/qiskit/quantum_info/states/statevector.py b/qiskit/quantum_info/states/statevector.py
--- a/qiskit/quantum_info/states/statevector.py
+++ b/qiskit/quantum_info/states/statevector.py
@@ -35,7 +35,11 @@ class Statevector(QuantumState):
 
     def __init__(self, data, dims=None):
         """Initialize a state object."""
-        if isinstance(data, Statevector):
+        if isinstance(data, (list, np.ndarray)):
+            # Finally we check if the input is a raw vector in either a
+            # python list or numpy array format.
+            self._data = np.asarray(data, dtype=complex)
+        elif isinstance(data, Statevector):
             self._data = data._data
             if dims is None:
                 dims = data._dims
@@ -45,20 +49,17 @@ def __init__(self, data, dims=None):
             if input_dim != 1:
                 raise QiskitError("Input Operator is not a column-vector.")
             self._data = np.ravel(data.data)
-        elif isinstance(data, (list, np.ndarray)):
-            # Finally we check if the input is a raw vector in either a
-            # python list or numpy array format.
-            self._data = np.asarray(data, dtype=complex)
         else:
             raise QiskitError("Invalid input data format for Statevector")
         # Check that the input is a numpy vector or column-vector numpy
         # matrix. If it is a column-vector matrix reshape to a vector.
         ndim = self._data.ndim
         shape = self._data.shape
-        if ndim not in [1, 2] or (ndim == 2 and shape[1] != 1):
-            raise QiskitError("Invalid input: not a vector or column-vector.")
-        if ndim == 2 and shape[1] == 1:
-            self._data = np.reshape(self._data, shape[0])
+        if ndim != 1:
+            if ndim == 2 and shape[1] == 1:
+                self._data = np.reshape(self._data, shape[0])
+            elif ndim != 2 or shape[1] != 1:
+                raise QiskitError("Invalid input: not a vector or column-vector.")
         super().__init__(self._automatic_dims(dims, shape[0]))
 
     def __eq__(self, other):
@@ -470,8 +471,8 @@ def from_instruction(cls, instruction):
             instruction = instruction.to_instruction()
         # Initialize an the statevector in the all |0> state
         init = np.zeros(2 ** instruction.num_qubits, dtype=complex)
-        init[0] = 1
-        vec = Statevector(init, dims=instruction.num_qubits * [2])
+        init[0] = 1.0
+        vec = Statevector(init, dims=instruction.num_qubits * (2,))
         vec._append_instruction(instruction)
         return vec
 
@@ -548,7 +549,8 @@ def _append_instruction(self, obj, qargs=None):
         if mat is not None:
             # Perform the composition and inplace update the current state
             # of the operator
-            self._data = self.evolve(mat, qargs=qargs).data
+            state = self.evolve(mat, qargs=qargs)
+            self._data = state.data
         else:
             # If the instruction doesn't have a matrix defined we use its
             # circuit decomposition definition if it exists, otherwise we


Test patch: