Repository: Qiskit/qiskit

Issue:
Gradients crash in case of a ListOp where elements have non-identical sets of parameters
<!-- ⚠️ 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**: master
- **Python version**: 3.8
- **Operating system**: MacOS

### What is the current behavior?

Suppose a `ListOp` that contains of 2 expected values (PauliSumOps with at least 2 Pauli terms) that are based on parametrized circuits with different parameter sets. Then, evaluating the ListOp is fine, but evaluating its gradient crashes since it first realizes that one element does not contain a certain parameter and replaces the partial derivative by 0.0 (float, not operator), which doesn't work in some of the following steps where the opflow assumes to get an operator and not a number (e.g. if it tries to `reduce` it or something like that). 

### Steps to reproduce the problem

The following code reproduces the problem:

```python
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.opflow import StateFn, CircuitStateFn, ListOp, PauliSumOp, Gradient

x, y = Parameter('x'), Parameter('y')
param_values = {x: 1, y: 3}

# observable
H = StateFn(PauliSumOp.from_list([('Z', 1.0), ('X', 1.0)]))

qc_x = QuantumCircuit(1)
qc_x.ry(x, 0)
op_x = ~H @ CircuitStateFn(qc_x)

qc_y = QuantumCircuit(1)
qc_y.ry(y, 0)
op_y = ~H @ CircuitStateFn(qc_y)

op = ListOp([op_x, op_y])

# evaluating the 2-element ListOp works
print(op.bind_parameters(param_values).eval())

# but evaluating its gradient crashes
gradient = Gradient()
grad_op = gradient.convert(op, [x, y])
grad_op.bind_parameters(param_values).eval()
```

### What is the expected behavior?

The correct behavior would not replace the partial derivative (lets say `d/dx <psi(y)|Hpsi(y)>` by 0.0 (float) but e.g. by `~Zero @ One`. 

### Suggested solutions

A possible workaround is to include the following lines into `qiskit/optlfow/gradients/gradient.py` (around line 170):

```python
[...]
# Handle the chain rule
elif isinstance(operator, ListOp):

    grad_ops = [self.get_gradient(op, param) for op in operator.oplist]

    # replace 0 by 0-operator --> although this works, it likely is not the right place for the fix
    for i, op in enumerate(grad_ops):
        if op == 0:
            grad_ops[i] = ~Zero @ One

    # Note: this check to see if the ListOp has a default combo_fn
    [...]
```




Discussion:

Resolving patch:
diff --git a/qiskit/opflow/list_ops/list_op.py b/qiskit/opflow/list_ops/list_op.py
--- a/qiskit/opflow/list_ops/list_op.py
+++ b/qiskit/opflow/list_ops/list_op.py
@@ -72,12 +72,19 @@ def __init__(self,
             identity - it accepts the list of values, and returns them in a list.
         """
         super().__init__()
-        self._oplist = list(oplist)
+        self._oplist = self._check_input_types(oplist)
         self._combo_fn = combo_fn
         self._coeff = coeff
         self._abelian = abelian
         self._grad_combo_fn = grad_combo_fn
 
+    def _check_input_types(self, oplist):
+        if all(isinstance(x, OperatorBase) for x in oplist):
+            return list(oplist)
+        else:
+            badval = next(x for x in oplist if not isinstance(x, OperatorBase))
+            raise TypeError(f'ListOp expecting objects of type OperatorBase, got {badval}')
+
     def _state(self,
                coeff: Optional[Union[complex, ParameterExpression]] = None,
                combo_fn: Optional[Callable] = None,
diff --git a/qiskit/opflow/list_ops/summed_op.py b/qiskit/opflow/list_ops/summed_op.py
--- a/qiskit/opflow/list_ops/summed_op.py
+++ b/qiskit/opflow/list_ops/summed_op.py
@@ -118,6 +118,9 @@ def reduce(self) -> OperatorBase:
         Returns:
             A collapsed version of self, if possible.
         """
+        if len(self.oplist) == 0:
+            return SummedOp([], coeff=self.coeff, abelian=self.abelian)
+
         # reduce constituents
         reduced_ops = sum(op.reduce() for op in self.oplist) * self.coeff
 


Test patch:
diff --git a/test/python/opflow/test_op_construction.py b/test/python/opflow/test_op_construction.py
--- a/test/python/opflow/test_op_construction.py
+++ b/test/python/opflow/test_op_construction.py
@@ -393,6 +393,10 @@ def test_summed_op_reduce(self):
             self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY', 'ZZ'])
             self.assertListEqual([op.coeff for op in sum_op], [10, 2, 3])
 
+        sum_op = SummedOp([])
+        with self.subTest('SummedOp test 9'):
+            self.assertEqual(sum_op.reduce(), sum_op)
+
     def test_compose_op_of_different_dim(self):
         """
         Test if smaller operator expands to correct dim when composed with bigger operator.