Repository: Qiskit/qiskit

Issue:
PauliExpectation warning given unexpectedly
When running the following test

https://github.com/Qiskit/qiskit-nature/blob/01ec334a18fbcf2368765dfd2d522e116da9de0e/test/algorithms/ground_state_solvers/test_groundstate_eigensolver.py#L334

it prints this message unexpectedly to me.

`Measured Observable is not composed of only Paulis, converting to Pauli representation, which can be expensive.`

In looking it seems to be when the expectation is taken of the aux_operators. This is a ListOp of PauliSumOps i.e. a List of a number of different operators like dipole X, num particles etc of which an expectation is computed against the result state of VQE to measure these.

Now at the time of writing this test is failing to meet the final result. That's a different problem than the above but in looking at that is when I saw this message appearing


Discussion:

Resolving patch:
diff --git a/qiskit/opflow/expectations/aer_pauli_expectation.py b/qiskit/opflow/expectations/aer_pauli_expectation.py
--- a/qiskit/opflow/expectations/aer_pauli_expectation.py
+++ b/qiskit/opflow/expectations/aer_pauli_expectation.py
@@ -52,7 +52,6 @@ def convert(self, operator: OperatorBase) -> OperatorBase:
         else:
             return operator
 
-    # pylint: disable=inconsistent-return-statements
     @classmethod
     def _replace_pauli_sums(cls, operator):
         try:
@@ -67,6 +66,8 @@ def _replace_pauli_sums(cls, operator):
         # CircuitSampler will look for it to know that the circuit is a Expectation
         # measurement, and not simply a
         # circuit to replace with a DictStateFn
+        if operator.__class__ == ListOp:
+            return operator.traverse(cls._replace_pauli_sums)
 
         if isinstance(operator, PauliSumOp):
             paulis = [(meas[1], meas[0]) for meas in operator.primitive.to_list()]
@@ -74,7 +75,7 @@ def _replace_pauli_sums(cls, operator):
             return CircuitStateFn(snapshot_instruction, coeff=operator.coeff, is_measurement=True)
 
         # Change to Pauli representation if necessary
-        if not {"Pauli"} == operator.primitive_strings():
+        if {"Pauli"} != operator.primitive_strings():
             logger.warning(
                 "Measured Observable is not composed of only Paulis, converting to "
                 "Pauli representation, which can be expensive."
@@ -93,8 +94,10 @@ def _replace_pauli_sums(cls, operator):
             snapshot_instruction = SnapshotExpectationValue("expval_measurement", paulis)
             snapshot_op = CircuitStateFn(snapshot_instruction, is_measurement=True)
             return snapshot_op
-        if isinstance(operator, ListOp):
-            return operator.traverse(cls._replace_pauli_sums)
+
+        raise TypeError(
+            f"Conversion of OperatorStateFn of {operator.__class__.__name__} is not defined."
+        )
 
     def compute_variance(self, exp_op: OperatorBase) -> Union[list, float]:
         r"""
@@ -116,5 +119,6 @@ def sum_variance(operator):
                 return 0.0
             elif isinstance(operator, ListOp):
                 return operator._combo_fn([sum_variance(op) for op in operator.oplist])
+            raise TypeError(f"Variance cannot be computed for {operator.__class__.__name__}.")
 
         return sum_variance(exp_op)
diff --git a/qiskit/opflow/expectations/pauli_expectation.py b/qiskit/opflow/expectations/pauli_expectation.py
--- a/qiskit/opflow/expectations/pauli_expectation.py
+++ b/qiskit/opflow/expectations/pauli_expectation.py
@@ -61,12 +61,15 @@ def convert(self, operator: OperatorBase) -> OperatorBase:
         Returns:
             The converted operator.
         """
+        if isinstance(operator, ListOp):
+            return operator.traverse(self.convert).reduce()
+
         if isinstance(operator, OperatorStateFn) and operator.is_measurement:
             # Change to Pauli representation if necessary
             if (
                 isinstance(operator.primitive, (ListOp, PrimitiveOp))
                 and not isinstance(operator.primitive, PauliSumOp)
-                and {"Pauli"} != operator.primitive_strings()
+                and {"Pauli", "SparsePauliOp"} < operator.primitive_strings()
             ):
                 logger.warning(
                     "Measured Observable is not composed of only Paulis, converting to "
@@ -86,9 +89,6 @@ def convert(self, operator: OperatorBase) -> OperatorBase:
             cob = PauliBasisChange(replacement_fn=PauliBasisChange.measurement_replacement_fn)
             return cob.convert(operator).reduce()
 
-        if isinstance(operator, ListOp):
-            return operator.traverse(self.convert).reduce()
-
         return operator
 
     def compute_variance(self, exp_op: OperatorBase) -> Union[list, float, np.ndarray]:


Test patch:
diff --git a/test/python/opflow/test_aer_pauli_expectation.py b/test/python/opflow/test_aer_pauli_expectation.py
--- a/test/python/opflow/test_aer_pauli_expectation.py
+++ b/test/python/opflow/test_aer_pauli_expectation.py
@@ -17,27 +17,28 @@
 from test.python.opflow import QiskitOpflowTestCase
 import numpy as np
 
+from qiskit.circuit.library import RealAmplitudes
 from qiskit.utils import QuantumInstance
 from qiskit.opflow import (
-    X,
-    Y,
-    Z,
-    I,
     CX,
+    AerPauliExpectation,
+    CircuitSampler,
+    CircuitStateFn,
     H,
-    S,
+    I,
     ListOp,
-    Zero,
+    Minus,
     One,
+    PauliExpectation,
+    PauliSumOp,
     Plus,
-    Minus,
+    S,
     StateFn,
-    AerPauliExpectation,
-    CircuitSampler,
-    CircuitStateFn,
-    PauliExpectation,
+    X,
+    Y,
+    Z,
+    Zero,
 )
-from qiskit.circuit.library import RealAmplitudes
 
 
 class TestAerPauliExpectation(QiskitOpflowTestCase):
@@ -243,6 +244,14 @@ def test_pauli_expectation_param_qobj(self):
         np.testing.assert_array_almost_equal(val1, val3, decimal=2)
         np.testing.assert_array_almost_equal([val1] * 2, val4, decimal=2)
 
+    def test_list_pauli_sum(self):
+        """Test AerPauliExpectation for ListOp[PauliSumOp]"""
+        test_op = ListOp([PauliSumOp.from_list([("XX", 1), ("ZI", 3), ("ZZ", 5)])])
+        observable = AerPauliExpectation().convert(~StateFn(test_op))
+        self.assertIsInstance(observable, ListOp)
+        self.assertIsInstance(observable[0], CircuitStateFn)
+        self.assertTrue(observable[0].is_measurement)
+
     def test_pauli_expectation_non_hermite_op(self):
         """Test PauliExpectation for non hermitian operator"""
         exp = ~StateFn(1j * Z) @ One
diff --git a/test/python/opflow/test_pauli_expectation.py b/test/python/opflow/test_pauli_expectation.py
--- a/test/python/opflow/test_pauli_expectation.py
+++ b/test/python/opflow/test_pauli_expectation.py
@@ -12,32 +12,32 @@
 
 """ Test PauliExpectation """
 
+import itertools
 import unittest
 from test.python.opflow import QiskitOpflowTestCase
-import itertools
+
 import numpy as np
 
-from qiskit.utils import QuantumInstance
-from qiskit.utils import algorithm_globals
+from qiskit import BasicAer
 from qiskit.opflow import (
-    X,
-    Y,
-    Z,
-    I,
     CX,
+    CircuitSampler,
     H,
-    S,
+    I,
     ListOp,
-    Zero,
+    Minus,
     One,
+    PauliExpectation,
+    PauliSumOp,
     Plus,
-    Minus,
+    S,
     StateFn,
-    PauliExpectation,
-    CircuitSampler,
+    X,
+    Y,
+    Z,
+    Zero,
 )
-
-from qiskit import BasicAer
+from qiskit.utils import QuantumInstance, algorithm_globals
 
 
 # pylint: disable=invalid-name
@@ -246,6 +246,14 @@ def test_pauli_expectation_non_hermite_op(self):
         exp = ~StateFn(1j * Z) @ One
         self.assertEqual(self.expect.convert(exp).eval(), 1j)
 
+    def test_list_pauli_sum_op(self):
+        """Test PauliExpectation for List[PauliSumOp]"""
+        test_op = ListOp([~StateFn(PauliSumOp.from_list([("XX", 1), ("ZI", 3), ("ZZ", 5)]))])
+        observable = self.expect.convert(test_op)
+        self.assertIsInstance(observable, ListOp)
+        self.assertIsInstance(observable[0][0][0].primitive, PauliSumOp)
+        self.assertIsInstance(observable[0][1][0].primitive, PauliSumOp)
+
 
 if __name__ == "__main__":
     unittest.main()