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

Fix NumPyEigensolver with SparsePauliOp #9101

Merged

Conversation

declanmillar
Copy link
Member

@declanmillar declanmillar commented Nov 8, 2022

Summary

Fixes an issue where NumPyEigensolver and by extension NumPyMinimumEigensolver did not support all BaseOperator instances, in particular SparsePauliOp. This was because it required the data attribute, which Operator exposes but e.g. SparsePauliOp does not. Closes #9091.

Details and comments

For all BaseOperator child classes, other than Operator, to_matrix is used to get the NumPy matrix for the operator and compute the eigenvalues and eigenstates. If an Operator instance, we still use data.

Additionally this PR:

  • Refactors NumPyEigensolver to be stateless w.r.t. results. I.e. the result is no longer stored.
  • Adds unit tests to check NumPyEigensolver with SparsePauliOps.
  • Adds units tests to check NumPyMinimumEigensolver with SparsePauliOps and Operators.
  • Uses sparse matrix computation for all operators that support it, otherwise try dense computation. If this fails and error is raised.

@qiskit-bot
Copy link
Collaborator

Thank you for opening a new pull request.

Before your PR can be merged it will first need to pass continuous integration tests and be reviewed. Sometimes the review process can be slow, so please be patient.

While you're waiting, please feel free to review other open PRs. While only a subset of people are authorized to approve pull requests for merging, everyone is encouraged to review open pull requests. Doing reviews helps reduce the burden on the core team and helps make the project's code better for everyone.

One or more of the the following people are requested to review this:

@coveralls
Copy link

coveralls commented Nov 8, 2022

Pull Request Test Coverage Report for Build 3873960414

  • 70 of 83 (84.34%) changed or added relevant lines in 2 files are covered.
  • 7 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-0.02%) to 84.535%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/algorithms/eigensolvers/numpy_eigensolver.py 68 81 83.95%
Files with Coverage Reduction New Missed Lines %
qiskit/extensions/hamiltonian_gate.py 1 89.23%
qiskit/extensions/unitary.py 1 95.45%
qiskit/algorithms/eigensolvers/numpy_eigensolver.py 5 84.21%
Totals Coverage Status
Change from base Build 3858894940: -0.02%
Covered Lines: 64032
Relevant Lines: 75746

💛 - Coveralls

@Cryoris
Copy link
Contributor

Cryoris commented Nov 9, 2022

It might be good to try using the sparse eigensolver (and the sparse matrix multiplication in the eval_aux_ops) when possible. Right now this is implemented for the PauliSumOp, but could definitely also be done for the SparsePauliOp (or maybe even all non-Operator operators?) by using to_matrix(sparse=True) 🙂

@declanmillar declanmillar force-pushed the fix-numpy-eigensolver-with-sparse-pauli-op branch from f022fd3 to 5c133d4 Compare November 9, 2022 14:57
@woodsp-ibm woodsp-ibm added Changelog: Bugfix Include in the "Fixed" section of the changelog mod: algorithms Related to the Algorithms module labels Nov 9, 2022
@declanmillar declanmillar force-pushed the fix-numpy-eigensolver-with-sparse-pauli-op branch from 5c133d4 to 7abf0c2 Compare November 9, 2022 21:12
Copy link
Contributor

@Cryoris Cryoris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, some minor comments on structure below !

qiskit/algorithms/eigensolvers/numpy_eigensolver.py Outdated Show resolved Hide resolved
qiskit/algorithms/eigensolvers/numpy_eigensolver.py Outdated Show resolved Hide resolved
qiskit/algorithms/eigensolvers/numpy_eigensolver.py Outdated Show resolved Hide resolved
@@ -110,68 +108,50 @@ def _check_set_k(self, operator: BaseOperator | PauliSumOp) -> None:
self._k = self._in_k

def _solve(self, operator: BaseOperator | PauliSumOp) -> None:
if isinstance(operator, PauliSumOp):
sp_mat = operator.to_spmatrix()
if isinstance(operator, Operator):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a more general comment. BaseOperator | PauliSumOp was chosen to be the same as that which Estimator supports. However from what I can see BaseOperator provides no standard way to access the data comprising the operator as needed here. An Operator has data, and Pauli types seems to have to_matrix(). ScalarOp is also a BaseOperator and while it has to_matrix does not have a sparse attribute hence would fail if used here. Looking at Estimator when it does an expectation, using StateVector, it may end up creating an Operator, depending on type, where its constructor inspects the object being passed for various aspects to build it out - I think ScalarOp would pass through this. I guess I am wondering more about the if its not an Operator and then its not PauliSumOp we are in the realm of all other BaseOperators that are not the Operator subclass thereof. The to_matrix(sparse=True) looks like for the Pauli based ones like PauliTable, SparsePauliOp that this is implemented in these - but again this is a specific set of these. This logic certainly now captures the Pauli types like SparsePauliOp of BaseOperator, which it did not before. Just wondering whether we can/should do better - maybe at least wrap the to_matrix(sparse=Try) in a try/except and raise perhaps an error if that can't be called that the operator type is not supported?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like a try/except is the best option, as there are several operator classes that do not subclass BasePauli but do allow sparse=True. It will also give a more appropriate error when a parameterized operator is passed in. From what I can see all BaseOperator classes, other than Operator, do expose a to_matrix method. Would it be worth adding this method to Operator and just returning data? Clifford and CNOTDihedral already effectively do this via to_operator. Could we perhaps also make to_matrix a required method of BaseOperator.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since primitives - especially the remote ones, I would think, need access to the data to be able to do the computation the fact that BaseOperator is the designated type but provides no defined mechanisms, indeed seems problematic. I am tagging @ikkoham and @t-imamichi to bring them into the conversation more generally. The reference Estimator relies on StateVector being able to compute an estimation of the operator (it and the operators being part of quantum_info) - for other primitives, that may use an aspect of it, like here, it can be different.

Here specifically, in the near term, I think a try/except to be able to raise a more meaningful error. Longer term it seems to me that whatever the type we indicate should have suitable method(s) for the purposes we have/envision. Whether BaseOperator (or something else) is updated or perhaps new mixin/Protocol(s) added seems like it should be the subject of a separate issue/PR to discuss and hopefully address these wider needs and revisit this try/catch later as warrants.

Copy link
Contributor

@ElePT ElePT left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code LGTM. I thought that maybe it would be possible to wrap all inputs in a SparsePauliOp not to have to deal with each case separately, but I could not find a better way to do it. I have followed a simular principle on this PR for VQD #9245.

@@ -109,69 +108,64 @@ def _check_set_k(self, operator: BaseOperator | PauliSumOp) -> None:
else:
self._k = self._in_k

def _solve(self, operator: BaseOperator | PauliSumOp) -> None:
def _solve(self, operator: BaseOperator | PauliSumOp) -> tuple[np.ndarray, np.ndarray]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, you could add a check here for num_qubits, and raise an error if it's None, to cover all of these operator types that don't make that much sense (like ScalarOp).

for key, operator in key_op_iterator:
if operator is None:
continue
value = 0.0

op_matrix = None
if isinstance(operator, PauliSumOp):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder that the check for num_qubits should also go here.

("ZZ", -0.01128010425623538),
("XX", 0.18093119978423156),
]
from qiskit.quantum_info import Operator, SparsePauliOp
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add one test for other BaseOperator subclasses? I'm not sure what others would be reasonable... Pauli? I don't think it's worth it to repeat all of the tests 7 times, but having a simple test for these extra clases would probably make sense.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are additional outliers that cause issues. For example PauliList and PauliTable, which also subclass BaseOperator but will cause an error in NumPyEigensolver. We could accommodate these, but they would require individual logic. This is easy enough, but seems like a lot of extra code for functionality that no one is likely to need.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't think it makes sense to add logic for edge cases... but we could check that the error raised is informative enough and these cases are accounted for (even if through raising an error).

@declanmillar declanmillar force-pushed the fix-numpy-eigensolver-with-sparse-pauli-op branch from ca03d5c to f6a37e7 Compare December 14, 2022 15:02
Copy link
Contributor

@ElePT ElePT left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! @Cryoris, if the CI doesn't break, I would say it's mergeable 😇

@declanmillar declanmillar force-pushed the fix-numpy-eigensolver-with-sparse-pauli-op branch from f6a37e7 to 1beb7c4 Compare December 14, 2022 16:32
@Cryoris Cryoris added this to the 0.23.0 milestone Dec 20, 2022
@mergify mergify bot merged commit 9b58d41 into Qiskit:main Jan 9, 2023
king-p3nguin pushed a commit to king-p3nguin/qiskit-terra that referenced this pull request Jan 11, 2023
* Fix NumPyEigensolver for SparsePauliOp

* Use to_matrix instead for non-Operator BaseOperator children.
* Make NumPyEigensolver stateless w.r.t. results.
* Build operators as SparsePauliOps rather than PauliSumOps.

* Test NumPyMinimumEigensolver with different ops

Test NumPyMinimumEigensolver with:

* SparsePauliOp
* Operator
* PauliSumOp

* add releasenote

* Use sparse matrix computation for non-Operator

* Use sparse matmul for non-Operator

* improve structure

* try dense matrix for all BaseOperators or raise ex

* remove unused import

* test Operator.to_matrix()

* use instance to access sparse

* raise error for invalid num_qubits. pauliop/scalarop tests

Co-authored-by: ElePT <57907331+ElePT@users.noreply.github.com>
Co-authored-by: Julien Gacon <gaconju@gmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Cryoris added a commit to Cryoris/qiskit-terra that referenced this pull request Jan 12, 2023
* Fix NumPyEigensolver for SparsePauliOp

* Use to_matrix instead for non-Operator BaseOperator children.
* Make NumPyEigensolver stateless w.r.t. results.
* Build operators as SparsePauliOps rather than PauliSumOps.

* Test NumPyMinimumEigensolver with different ops

Test NumPyMinimumEigensolver with:

* SparsePauliOp
* Operator
* PauliSumOp

* add releasenote

* Use sparse matrix computation for non-Operator

* Use sparse matmul for non-Operator

* improve structure

* try dense matrix for all BaseOperators or raise ex

* remove unused import

* test Operator.to_matrix()

* use instance to access sparse

* raise error for invalid num_qubits. pauliop/scalarop tests

Co-authored-by: ElePT <57907331+ElePT@users.noreply.github.com>
Co-authored-by: Julien Gacon <gaconju@gmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
@declanmillar declanmillar deleted the fix-numpy-eigensolver-with-sparse-pauli-op branch January 17, 2023 15:30
ElePT added a commit to ElePT/qiskit that referenced this pull request Jun 27, 2023
* Fix NumPyEigensolver for SparsePauliOp

* Use to_matrix instead for non-Operator BaseOperator children.
* Make NumPyEigensolver stateless w.r.t. results.
* Build operators as SparsePauliOps rather than PauliSumOps.

* Test NumPyMinimumEigensolver with different ops

Test NumPyMinimumEigensolver with:

* SparsePauliOp
* Operator
* PauliSumOp

* add releasenote

* Use sparse matrix computation for non-Operator

* Use sparse matmul for non-Operator

* improve structure

* try dense matrix for all BaseOperators or raise ex

* remove unused import

* test Operator.to_matrix()

* use instance to access sparse

* raise error for invalid num_qubits. pauliop/scalarop tests

Co-authored-by: ElePT <57907331+ElePT@users.noreply.github.com>
Co-authored-by: Julien Gacon <gaconju@gmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
ElePT added a commit to ElePT/qiskit-algorithms-test that referenced this pull request Jul 17, 2023
* Fix NumPyEigensolver for SparsePauliOp

* Use to_matrix instead for non-Operator BaseOperator children.
* Make NumPyEigensolver stateless w.r.t. results.
* Build operators as SparsePauliOps rather than PauliSumOps.

* Test NumPyMinimumEigensolver with different ops

Test NumPyMinimumEigensolver with:

* SparsePauliOp
* Operator
* PauliSumOp

* add releasenote

* Use sparse matrix computation for non-Operator

* Use sparse matmul for non-Operator

* improve structure

* try dense matrix for all BaseOperators or raise ex

* remove unused import

* test Operator.to_matrix()

* use instance to access sparse

* raise error for invalid num_qubits. pauliop/scalarop tests

Co-authored-by: ElePT <57907331+ElePT@users.noreply.github.com>
Co-authored-by: Julien Gacon <gaconju@gmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: Bugfix Include in the "Fixed" section of the changelog mod: algorithms Related to the Algorithms module
Projects
None yet
Development

Successfully merging this pull request may close these issues.

NumPyEigensolver does not support all BaseOperator instances.
6 participants