Repository: Qiskit/qiskit

Issue:
Statevector.evolve works incorrectly for subsystem evolution on mixed-dimension subsystems
### Environment

- **Qiskit Terra version**: 0.21
- **Python version**: All
- **Operating system**: All

### What is happening?

`Statevector.evolve` returns the incorrect answer when evolving certain subsystems if the statevector has mixed dimension subsystems. This is due to it incorrectly reshaping the temporary transposed tensors during the evolution contraction when flattening back to a returned statevector.

### How can we reproduce the issue?

```python
import qiskit.quantum_info as qi
import numpy as np

# Mixed dimension statevector
dims = (3, 4, 5)
state = qi.random_statevector(dims, seed=123)

# Evolve subsystem zero by an operator
op = qi.random_unitary(dims[:1], seed=123)
evo = state.evolve(op, [0])

# Compare to full system evolution
mat = op.expand(np.eye(np.prod(dims[1:]))).data
target = qi.Statevector(np.dot(mat, state.data), dims)

# Fidelity should be 1
qi.state_fidelity(evo, target)

0.011873940293697585
```

### What should happen?

If things were working correctly the above code snippet should return a fidelity of 1

### Any suggestions?

The error is in the `Statevector._evolve_operator` method where one of the `np.reshape` instructions is given an incorrect dimension specification in certain cases where there are >= 2 non-evolved subsystems with different dimensions.


Discussion:

Resolving patch:
diff --git a/qiskit/quantum_info/operators/op_shape.py b/qiskit/quantum_info/operators/op_shape.py
--- a/qiskit/quantum_info/operators/op_shape.py
+++ b/qiskit/quantum_info/operators/op_shape.py
@@ -463,6 +463,7 @@ def compose(self, other, qargs=None, front=False):
                 for i, dim in zip(qargs, other.dims_r()):
                     dims_r[i] = dim
                 ret._dims_r = tuple(dims_r)
+                ret._num_qargs_r = len(ret._dims_r)
             else:
                 ret._num_qargs_r = self._num_qargs_r
         else:
@@ -484,6 +485,7 @@ def compose(self, other, qargs=None, front=False):
                 for i, dim in zip(qargs, other.dims_l()):
                     dims_l[i] = dim
                 ret._dims_l = tuple(dims_l)
+                ret._num_qargs_l = len(ret._dims_l)
             else:
                 ret._num_qargs_l = self._num_qargs_l
         return ret
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
@@ -329,7 +329,8 @@ def evolve(self, other, qargs=None):
 
         # Unitary evolution by an Operator
         if not isinstance(other, Operator):
-            other = Operator(other)
+            dims = self.dims(qargs=qargs)
+            other = Operator(other, input_dims=dims, output_dims=dims)
         return self._evolve_operator(other, qargs=qargs)
 
     def reverse_qargs(self):
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
@@ -388,7 +388,8 @@ def evolve(self, other, qargs=None):
 
         # Evolution by an Operator
         if not isinstance(other, Operator):
-            other = Operator(other)
+            dims = self.dims(qargs=qargs)
+            other = Operator(other, input_dims=dims, output_dims=dims)
 
         # check dimension
         if self.dims(qargs) != other.input_dims():
@@ -818,14 +819,21 @@ def _evolve_operator(statevec, oper, qargs=None):
         contract_dim = oper._op_shape.shape[1]
         contract_shape = (contract_dim, statevec._op_shape.shape[0] // contract_dim)
 
-        # Reshape input for contraction
-        statevec._data = np.reshape(
-            np.transpose(np.reshape(statevec.data, statevec._op_shape.tensor_shape), axes),
-            contract_shape,
+        # Reshape and transpose input array for contraction
+        tensor = np.transpose(
+            np.reshape(statevec.data, statevec._op_shape.tensor_shape),
+            axes,
         )
-        # Contract and reshape output
-        statevec._data = np.reshape(np.dot(oper.data, statevec._data), new_shape.tensor_shape)
-        statevec._data = np.reshape(np.transpose(statevec._data, axes_inv), new_shape.shape[0])
+        tensor_shape = tensor.shape
+
+        # Perform contraction
+        tensor = np.reshape(
+            np.dot(oper.data, np.reshape(tensor, contract_shape)),
+            tensor_shape,
+        )
+
+        # Transpose back to  original subsystem spec and flatten
+        statevec._data = np.reshape(np.transpose(tensor, axes_inv), new_shape.shape[0])
 
         # Update dimension
         statevec._op_shape = new_shape


Test patch:
diff --git a/test/python/quantum_info/states/test_densitymatrix.py b/test/python/quantum_info/states/test_densitymatrix.py
--- a/test/python/quantum_info/states/test_densitymatrix.py
+++ b/test/python/quantum_info/states/test_densitymatrix.py
@@ -284,6 +284,20 @@ def test_evolve_subsystem(self):
             target = DensityMatrix(np.dot(op_full.data, rho).dot(op_full.adjoint().data))
             self.assertEqual(state.evolve(op, qargs=[2, 1, 0]), target)
 
+    def test_evolve_qudit_subsystems(self):
+        """Test nested evolve calls on qudit subsystems."""
+        dims = (3, 4, 5)
+        init = self.rand_rho(np.prod(dims))
+        ops = [random_unitary((dim,)) for dim in dims]
+        state = DensityMatrix(init, dims)
+        for i, op in enumerate(ops):
+            state = state.evolve(op, [i])
+        target_op = np.eye(1)
+        for op in ops:
+            target_op = np.kron(op.data, target_op)
+        target = DensityMatrix(np.dot(target_op, init).dot(target_op.conj().T), dims)
+        self.assertEqual(state, target)
+
     def test_conjugate(self):
         """Test conjugate method."""
         for _ in range(10):
diff --git a/test/python/quantum_info/states/test_statevector.py b/test/python/quantum_info/states/test_statevector.py
--- a/test/python/quantum_info/states/test_statevector.py
+++ b/test/python/quantum_info/states/test_statevector.py
@@ -310,6 +310,20 @@ def test_evolve_subsystem(self):
             target = Statevector(np.dot(op_full.data, vec))
             self.assertEqual(state.evolve(op, qargs=[2, 1, 0]), target)
 
+    def test_evolve_qudit_subsystems(self):
+        """Test nested evolve calls on qudit subsystems."""
+        dims = (3, 4, 5)
+        init = self.rand_vec(np.prod(dims))
+        ops = [random_unitary((dim,)) for dim in dims]
+        state = Statevector(init, dims)
+        for i, op in enumerate(ops):
+            state = state.evolve(op, [i])
+        target_op = np.eye(1)
+        for op in ops:
+            target_op = np.kron(op.data, target_op)
+        target = Statevector(np.dot(target_op, init), dims)
+        self.assertEqual(state, target)
+
     def test_evolve_global_phase(self):
         """Test evolve circuit with global phase."""
         state_i = Statevector([1, 0])