Repository: Qiskit/qiskit

Issue:
implement iterating/collecting PauliSumOp coefficients
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

It would be useful to be able to iterate over, or return a list of coefficients in PauliSumOp. At present, one has to dig into the details to construct the coefficients for each term. In particular, it requires multiplying two numbers for each coeffcient. There are use cases, for instance when bounding the eigenvalues of the operators.

For example, this gives a bound on the eigenvalues:
```python
float(sum(abs(pauli_sum.primitive.coeffs)) * abs(pauli_sum.coeff))
```

Abstracting access would be cleaner and resistant to implementation changes. Like this, if we want to return an numpy array
```python
float(sum(abs(pauli_sum.coeffs)))
```
or this, if we return a list
```python
sum(abs(c) for c in pauli_sum.coeffs)
```


This should be quite easy to implement.

https://github.com/Qiskit/qiskit-terra/blob/master/qiskit/opflow/primitive_ops/pauli_sum_op.py



Discussion:
Hello,

I'm pretty new to quantum, but I wanted to start contributing.

If this is the patch you were looking for, I can create a pull request.

```
diff --git a/qiskit/opflow/primitive_ops/pauli_sum_op.py b/qiskit/opflow/primitive_ops/pauli_sum_op.py
index 0b5c0526..b4ee9399 100644
--- a/qiskit/opflow/primitive_ops/pauli_sum_op.py
+++ b/qiskit/opflow/primitive_ops/pauli_sum_op.py
@@ -58,7 +58,13 @@ class PauliSumOp(PrimitiveOp):
     @property
     def num_qubits(self) -> int:
         return self.primitive.num_qubits  # type: ignore
-
+    
+    # issue 5547 start
+    @property
+    def coeffs(self):
+        return self.primitive.coeffs
+    # issue 5547 end
+    
     def add(self, other: OperatorBase) -> OperatorBase:
         if not self.num_qubits == other.num_qubits:
             raise ValueError(
```

Here's how it is working now:

```
a_table = PauliTable.from_labels(['X'])
pauli_sum = PauliSumOp(SparsePauliOp(a_table),1)
print(pauli_sum.coeffs)
print(type(pauli_sum.coeffs))

[1.+0.j]
<class 'numpy.ndarray'>
```

Paolo



Thanks for jumping in. I don't think that patch gives the correct result in all cases. There are two "layers" of coefficients: `pauli_sum.primitive.coeffs` and a single number `pauli_sum.coeff`.

 A list of the full coefficient might be generated like this: `[c * pauli_sum.coeff for c in pauli_sum.primitive.coeffs]`
Thank you for the additional info. Sorry I did not get it in the first place. So how about this?

```
diff --git a/qiskit/opflow/primitive_ops/pauli_sum_op.py b/qiskit/opflow/primitive_ops/pauli_sum_op.py
index 0b5c0526..32538152 100644
--- a/qiskit/opflow/primitive_ops/pauli_sum_op.py
+++ b/qiskit/opflow/primitive_ops/pauli_sum_op.py
@@ -59,6 +59,12 @@ class PauliSumOp(PrimitiveOp):
     def num_qubits(self) -> int:
         return self.primitive.num_qubits  # type: ignore
 
+    # issue 5547 start
+    @property
+    def coeffs(self):
+        return self.coeff * self.primitive.coeffs
+    # issue 5547 end
+
     def add(self, other: OperatorBase) -> OperatorBase:
         if not self.num_qubits == other.num_qubits:
             raise ValueError(
```
that would return something like

```
from qiskit.quantum_info import SparsePauliOp
from qiskit.opflow.primitive_ops import PauliSumOp
from qiskit.quantum_info.operators import PauliTable

a_table = PauliTable.from_labels(['X','I','Y'])

pauli_sum = PauliSumOp(SparsePauliOp(a_table),3)

print(pauli_sum.coeffs)

print(type(pauli_sum.coeffs))

[3.+0.j 3.+0.j 3.+0.j]
<class 'numpy.ndarray'>
```
Thanks, this looks good. Before making a PR, it would be a good idea to wait a bit to see if someone comments. There might be a reason to return a list rather than an array.
All right. Thanks a lot. I'll just leave this here for others to jump in and provide review/direction.

```python
diff --git a/qiskit/opflow/primitive_ops/pauli_sum_op.py b/qiskit/opflow/primitive_ops/pauli_sum_op.py
index 0b5c0526..89ec23ec 100644
--- a/qiskit/opflow/primitive_ops/pauli_sum_op.py
+++ b/qiskit/opflow/primitive_ops/pauli_sum_op.py
@@ -59,6 +59,16 @@ class PauliSumOp(PrimitiveOp):
     def num_qubits(self) -> int:
         return self.primitive.num_qubits  # type: ignore
 
+    # issue 5547 start
+    @property
+    def coeffs(self):
+        return self.coeff * self.primitive.coeffs
+
+    @property
+    def coeffslist(self):
+        return (self.coeff * self.primitive.coeffs).tolist()
+    # issue 5547 end
+
     def add(self, other: OperatorBase) -> OperatorBase:
         if not self.num_qubits == other.num_qubits:
             raise ValueError(

```

```python
from qiskit.quantum_info import SparsePauliOp
from qiskit.opflow.primitive_ops import PauliSumOp
from qiskit.quantum_info.operators import PauliTable

a_table = PauliTable.from_labels(['X','I','Y'])

pauli_sum = PauliSumOp(SparsePauliOp(a_table),3)

print(pauli_sum.coeffs)
print(type(pauli_sum.coeffs))

print(pauli_sum.coeffslist)
print(type(pauli_sum.coeffslist))
```
```
[3.+0.j 3.+0.j 3.+0.j]
<class 'numpy.ndarray'>
[(3+0j), (3+0j), (3+0j)]
<class 'list'>
```
Thanks. Nice. I provide some discussion points.
1. Are the internal types of lists python Native numeric type? or numpy.complex128? (Which is better?)
2. How about returning an iterator like https://github.com/Qiskit/qiskit-terra/blob/master/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py#L549-L575.
1. Items in a list can be anything. It depends on what produced them. Note that `numpy.ndarray.tolist` converts the numpy.complex128, etc to    builtin Python `complex`. My guess is that it is kind of standard to convert to builtin types if you are converting to (or building) a list.
2. I believe an iterator is used there in order to be memory efficient, since each matrix may be very large. Not to say an iterator would be wrong here, but I don't think the justification would be the same.
Hi thanks for the comments.

1. we could avoid the conversion and return a list of `numpy.complex128` with 
```python
return list(self.coeff * self.primitive.coeffs)
```
instead of calling the `tolist()` 

2. I will try to implement that iterator as per the suggested code and will post it here. I might need to come back for help on that.

I don't have any experience with this matter, so forgive me, but I'd keep it simple and go with the basic implementation of returning the coeffs with a numpy array, since that is the type that is returned by the primitive object, and leave any typecasting/conversion to the caller.
So, here's what I have so far:

```python
diff --git a/qiskit/opflow/primitive_ops/pauli_sum_op.py b/qiskit/opflow/primitive_ops/pauli_sum_op.py
index 0b5c0526..6bf43fd5 100644
--- a/qiskit/opflow/primitive_ops/pauli_sum_op.py
+++ b/qiskit/opflow/primitive_ops/pauli_sum_op.py
@@ -20,6 +20,10 @@ from scipy.sparse import spmatrix
 
 from qiskit.circuit import Instruction, ParameterExpression
 from qiskit.quantum_info import Pauli, SparsePauliOp
+# issue 5547 start
+from qiskit.quantum_info.operators.symplectic.pauli_table import PauliTable
+from qiskit.quantum_info.operators.custom_iterator import CustomIterator
+# issue 5547 end
 from ..exceptions import OpflowError
 from ..list_ops.summed_op import SummedOp
 from ..list_ops.tensored_op import TensoredOp
@@ -59,6 +63,47 @@ class PauliSumOp(PrimitiveOp):
     def num_qubits(self) -> int:
         return self.primitive.num_qubits  # type: ignore
 
+    # issue 5547 start
+    @property
+    def coeffs(self):
+        return self.coeff * self.primitive.coeffs
+
+    @property
+    def coeffslist(self):
+        # return (self.coeff * self.primitive.coeffs).tolist()
+        return list(self.coeff * self.primitive.coeffs)
+
+    def matrix_iter(self, sparse=False):
+        """Return a matrix representation iterator.
+
+        This is a lazy iterator that converts each term in the PauliSumOp
+        into a matrix as it is used. To convert to a single matrix use the
+        :meth:`to_matrix` method.
+
+        Args:
+            sparse (bool): optionally return sparse CSR matrices if True,
+                           otherwise return Numpy array matrices
+                           (Default: False)
+
+        Returns:
+            MatrixIterator: matrix iterator object for the PauliTable.
+        """
+        class MatrixIterator(CustomIterator):
+            """Matrix representation iteration and item access."""
+            def __repr__(self):
+                return "<PauliSumOp_matrix_iterator at {}>".format(hex(id(self)))
+
+
+            def __getitem__(self, key):
+                sumopcoeff = self.obj.coeff * self.obj.primitive.coeffs[key]
+                mat = PauliTable._to_matrix(self.obj.primitive.table.array[key],
+                                            sparse=sparse)
+                return sumopcoeff * mat
+
+
+        return MatrixIterator(self)
+    # issue 5547 end
+
     def add(self, other: OperatorBase) -> OperatorBase:
         if not self.num_qubits == other.num_qubits:
             raise ValueError(
```

```python
from qiskit.quantum_info import SparsePauliOp
from qiskit.opflow.primitive_ops import PauliSumOp
from qiskit.quantum_info.operators import PauliTable

a_table = PauliTable.from_labels(['X','I','Y'])

pauli_sum = PauliSumOp(SparsePauliOp(a_table),3)

print(pauli_sum.coeffs)
print(type(pauli_sum.coeffs))

print(pauli_sum.coeffslist)
print(type(pauli_sum.coeffslist[0]))

the_iterator = pauli_sum.matrix_iter()

print(the_iterator)
while True:
    try:
        print(next(the_iterator))
    except StopIteration:
        break
```

```
[3.+0.j 3.+0.j 3.+0.j]
<class 'numpy.ndarray'>
[(3+0j), (3+0j), (3+0j)]
<class 'numpy.complex128'>
<PauliSumOp_matrix_iterator at 0x7fef37701ee0>
[[0.+0.j 3.+0.j]
 [3.+0.j 0.+0.j]]
[[3.+0.j 0.+0.j]
 [0.+0.j 3.+0.j]]
[[0.+0.j 0.-3.j]
 [0.+3.j 0.+0.j]]
```

Resolving patch:
diff --git a/qiskit/opflow/primitive_ops/pauli_sum_op.py b/qiskit/opflow/primitive_ops/pauli_sum_op.py
--- a/qiskit/opflow/primitive_ops/pauli_sum_op.py
+++ b/qiskit/opflow/primitive_ops/pauli_sum_op.py
@@ -20,6 +20,8 @@
 
 from qiskit.circuit import Instruction, ParameterExpression
 from qiskit.quantum_info import Pauli, SparsePauliOp
+from qiskit.quantum_info.operators.symplectic.pauli_table import PauliTable
+from qiskit.quantum_info.operators.custom_iterator import CustomIterator
 from ..exceptions import OpflowError
 from ..list_ops.summed_op import SummedOp
 from ..list_ops.tensored_op import TensoredOp
@@ -59,6 +61,39 @@ def primitive_strings(self) -> Set[str]:
     def num_qubits(self) -> int:
         return self.primitive.num_qubits  # type: ignore
 
+    @property
+    def coeffs(self):
+        """Return the Pauli coefficients."""
+        return self.coeff * self.primitive.coeffs
+
+    def matrix_iter(self, sparse=False):
+        """Return a matrix representation iterator.
+
+        This is a lazy iterator that converts each term in the PauliSumOp
+        into a matrix as it is used. To convert to a single matrix use the
+        :meth:`to_matrix` method.
+
+        Args:
+            sparse (bool): optionally return sparse CSR matrices if True,
+                           otherwise return Numpy array matrices
+                           (Default: False)
+
+        Returns:
+            MatrixIterator: matrix iterator object for the PauliTable.
+        """
+        class MatrixIterator(CustomIterator):
+            """Matrix representation iteration and item access."""
+            def __repr__(self):
+                return "<PauliSumOp_matrix_iterator at {}>".format(hex(id(self)))
+
+            def __getitem__(self, key):
+                sumopcoeff = self.obj.coeff * self.obj.primitive.coeffs[key]
+                mat = PauliTable._to_matrix(self.obj.primitive.table.array[key],
+                                            sparse=sparse)
+                return sumopcoeff * mat
+
+        return MatrixIterator(self)
+
     def add(self, other: OperatorBase) -> OperatorBase:
         if not self.num_qubits == other.num_qubits:
             raise ValueError(


Test patch:
diff --git a/test/python/opflow/test_pauli_sum_op.py b/test/python/opflow/test_pauli_sum_op.py
--- a/test/python/opflow/test_pauli_sum_op.py
+++ b/test/python/opflow/test_pauli_sum_op.py
@@ -33,7 +33,7 @@
     Zero,
 )
 from qiskit.circuit import Parameter, ParameterVector
-from qiskit.quantum_info import Pauli, SparsePauliOp
+from qiskit.quantum_info import Pauli, SparsePauliOp, PauliTable
 
 
 class TestPauliSumOp(QiskitOpflowTestCase):
@@ -229,6 +229,30 @@ def test_from_list(self):
         )
         self.assertEqual(target, expected)
 
+    def test_matrix_iter(self):
+        """Test PauliSumOp dense matrix_iter method."""
+        labels = ['III', 'IXI', 'IYY', 'YIZ', 'XYZ', 'III']
+        coeffs = np.array([1, 2, 3, 4, 5, 6])
+        table = PauliTable.from_labels(labels)
+        coeff = 10
+        op = PauliSumOp(SparsePauliOp(table, coeffs), coeff)
+        for idx, i in enumerate(op.matrix_iter()):
+            self.assertTrue(
+                np.array_equal(i, coeff * coeffs[idx] *
+                               Pauli(labels[idx]).to_matrix()))
+
+    def test_matrix_iter_sparse(self):
+        """Test PauliSumOp sparse matrix_iter method."""
+        labels = ['III', 'IXI', 'IYY', 'YIZ', 'XYZ', 'III']
+        coeffs = np.array([1, 2, 3, 4, 5, 6])
+        coeff = 10
+        table = PauliTable.from_labels(labels)
+        op = PauliSumOp(SparsePauliOp(table, coeffs), coeff)
+        for idx, i in enumerate(op.matrix_iter(sparse=True)):
+            self.assertTrue(
+                np.array_equal(i.toarray(), coeff * coeffs[idx] *
+                               Pauli(labels[idx]).to_matrix()))
+
 
 if __name__ == "__main__":
     unittest.main()