Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement iterating/collecting PauliSumOp coefficients #5547

Closed
jlapeyre opened this issue Dec 18, 2020 · 9 comments · Fixed by #5560
Closed

implement iterating/collecting PauliSumOp coefficients #5547

jlapeyre opened this issue Dec 18, 2020 · 9 comments · Fixed by #5560
Labels
good first issue Good for newcomers type: enhancement It's working, but needs polishing

Comments

@jlapeyre
Copy link
Contributor

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:

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

float(sum(abs(pauli_sum.coeffs)))

or this, if we return a list

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

@jlapeyre jlapeyre added type: enhancement It's working, but needs polishing good first issue Good for newcomers labels Dec 18, 2020
@paolob67
Copy link
Contributor

paolob67 commented Dec 19, 2020

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

@jlapeyre
Copy link
Contributor Author

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]

@paolob67
Copy link
Contributor

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'>

@jlapeyre
Copy link
Contributor Author

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.

@paolob67
Copy link
Contributor

All right. Thanks a lot. I'll just leave this here for others to jump in and provide review/direction.

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(
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'>

@ikkoham
Copy link
Contributor

ikkoham commented Dec 21, 2020

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.

@jlapeyre
Copy link
Contributor Author

  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.

@paolob67
Copy link
Contributor

Hi thanks for the comments.

  1. we could avoid the conversion and return a list of numpy.complex128 with
return list(self.coeff * self.primitive.coeffs)

instead of calling the tolist()

  1. 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.

@paolob67
Copy link
Contributor

paolob67 commented Dec 21, 2020

So, here's what I have so far:

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(
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]]

paolob67 added a commit to paolob67/qiskit-terra that referenced this issue Dec 25, 2020
@mergify mergify bot closed this as completed in #5560 Jan 12, 2021
mergify bot pushed a commit that referenced this issue Jan 12, 2021
* Implement iterating/collecting PauliSumOp coefficients #5547

* Fixing Style and lint pipeline error on pull request

* Using Pauli(keys).to_matrix() for testing iterator

* removing unused import

Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com>
Co-authored-by: Julien Gacon <gaconju@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
good first issue Good for newcomers type: enhancement It's working, but needs polishing
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants