Skip to content

Commit 9b58d41

Browse files
declanmillarElePTCryorismergify[bot]
authored
Fix NumPyEigensolver with SparsePauliOp (#9101)
* 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>
1 parent 27da80d commit 9b58d41

File tree

7 files changed

+229
-187
lines changed

7 files changed

+229
-187
lines changed

qiskit/algorithms/eigensolvers/numpy_eigensolver.py

+114-97
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@
2020
from scipy import sparse as scisparse
2121

2222
from qiskit.opflow import PauliSumOp
23+
from qiskit.quantum_info import SparsePauliOp, Statevector
2324
from qiskit.quantum_info.operators.base_operator import BaseOperator
24-
from qiskit.quantum_info import Statevector
2525
from qiskit.utils.validation import validate_min
2626

2727
from .eigensolver import Eigensolver, EigensolverResult
28+
from ..exceptions import AlgorithmError
2829
from ..list_or_dict import ListOrDict
2930

3031
logger = logging.getLogger(__name__)
@@ -53,12 +54,12 @@ def __init__(
5354
) -> None:
5455
"""
5556
Args:
56-
k: number of eigenvalues are to be computed, with a min. value of 1.
57-
filter_criterion: callable that allows to filter eigenvalues/eigenstates. Only feasible
57+
k: Number of eigenvalues are to be computed, with a minimum value of 1.
58+
filter_criterion: Callable that allows to filter eigenvalues/eigenstates. Only feasible
5859
eigenstates are returned in the results. The callable has the signature
5960
``filter(eigenstate, eigenvalue, aux_values)`` and must return a boolean to indicate
6061
whether to keep this value in the final returned result or not. If the number of
61-
elements that satisfies the criterion is smaller than `k`, then the returned list will
62+
elements that satisfies the criterion is smaller than ``k``, then the returned list will
6263
have fewer elements and can even be empty.
6364
"""
6465
validate_min("k", k, 1)
@@ -69,8 +70,6 @@ def __init__(
6970

7071
self._filter_criterion = filter_criterion
7172

72-
self._ret = NumPyEigensolverResult()
73-
7473
@property
7574
def k(self) -> int:
7675
"""Return k (number of eigenvalues requested)."""
@@ -109,69 +108,64 @@ def _check_set_k(self, operator: BaseOperator | PauliSumOp) -> None:
109108
else:
110109
self._k = self._in_k
111110

112-
def _solve(self, operator: BaseOperator | PauliSumOp) -> None:
111+
def _solve(self, operator: BaseOperator | PauliSumOp) -> tuple[np.ndarray, np.ndarray]:
113112
if isinstance(operator, PauliSumOp):
114-
sp_mat = operator.to_spmatrix()
113+
op_matrix = operator.to_spmatrix()
114+
else:
115+
try:
116+
op_matrix = operator.to_matrix(sparse=True)
117+
except TypeError:
118+
logger.debug(
119+
"WARNING: operator of type `%s` does not support sparse matrices. "
120+
"Trying dense computation",
121+
type(operator),
122+
)
123+
try:
124+
op_matrix = operator.to_matrix()
125+
except AttributeError as ex:
126+
raise AlgorithmError(f"Unsupported operator type `{type(operator)}`.") from ex
127+
128+
if isinstance(op_matrix, scisparse.csr_matrix):
115129
# If matrix is diagonal, the elements on the diagonal are the eigenvalues. Solve by sorting.
116-
if scisparse.csr_matrix(sp_mat.diagonal()).nnz == sp_mat.nnz:
117-
diag = sp_mat.diagonal()
130+
if scisparse.csr_matrix(op_matrix.diagonal()).nnz == op_matrix.nnz:
131+
diag = op_matrix.diagonal()
118132
indices = np.argsort(diag)[: self._k]
119133
eigval = diag[indices]
120-
eigvec = np.zeros((sp_mat.shape[0], self._k))
134+
eigvec = np.zeros((op_matrix.shape[0], self._k))
121135
for i, idx in enumerate(indices):
122136
eigvec[idx, i] = 1.0
123137
else:
124138
if self._k >= 2**operator.num_qubits - 1:
125139
logger.debug(
126140
"SciPy doesn't support to get all eigenvalues, using NumPy instead."
127141
)
128-
if operator.is_hermitian():
129-
eigval, eigvec = np.linalg.eigh(operator.to_matrix())
130-
else:
131-
eigval, eigvec = np.linalg.eig(operator.to_matrix())
142+
eigval, eigvec = self._solve_dense(operator.to_matrix())
132143
else:
133-
if operator.is_hermitian():
134-
eigval, eigvec = scisparse.linalg.eigsh(sp_mat, k=self._k, which="SA")
135-
else:
136-
eigval, eigvec = scisparse.linalg.eigs(sp_mat, k=self._k, which="SR")
137-
138-
indices = np.argsort(eigval)[: self._k]
139-
eigval = eigval[indices]
140-
eigvec = eigvec[:, indices]
144+
eigval, eigvec = self._solve_sparse(op_matrix, self._k)
141145
else:
142-
logger.debug("SciPy not supported, using NumPy instead.")
143-
144-
if operator.data.all() == operator.data.conj().T.all():
145-
eigval, eigvec = np.linalg.eigh(operator.data)
146-
else:
147-
eigval, eigvec = np.linalg.eig(operator.data)
148-
149-
indices = np.argsort(eigval)[: self._k]
150-
eigval = eigval[indices]
151-
eigvec = eigvec[:, indices]
152-
153-
self._ret.eigenvalues = eigval
154-
self._ret.eigenstates = eigvec.T
146+
# Sparse SciPy matrix not supported, use dense NumPy computation.
147+
eigval, eigvec = self._solve_dense(operator.to_matrix())
155148

156-
def _get_ground_state_energy(self, operator: BaseOperator | PauliSumOp) -> None:
157-
if self._ret.eigenvalues is None or self._ret.eigenstates is None:
158-
self._solve(operator)
149+
indices = np.argsort(eigval)[: self._k]
150+
eigval = eigval[indices]
151+
eigvec = eigvec[:, indices]
152+
return eigval, eigvec.T
159153

160-
def _get_energies(
161-
self,
162-
operator: BaseOperator | PauliSumOp,
163-
aux_operators: ListOrDict[BaseOperator | PauliSumOp] | None,
164-
) -> None:
165-
if self._ret.eigenvalues is None or self._ret.eigenstates is None:
166-
self._solve(operator)
154+
@staticmethod
155+
def _solve_sparse(op_matrix: scisparse.csr_matrix, k: int) -> tuple[np.ndarray, np.ndarray]:
156+
if (op_matrix != op_matrix.H).nnz == 0:
157+
# Operator is Hermitian
158+
return scisparse.linalg.eigsh(op_matrix, k=k, which="SA")
159+
else:
160+
return scisparse.linalg.eigs(op_matrix, k=k, which="SR")
167161

168-
if aux_operators is not None:
169-
aux_op_vals = []
170-
for i in range(self._k):
171-
aux_op_vals.append(
172-
self._eval_aux_operators(aux_operators, self._ret.eigenstates[i])
173-
)
174-
self._ret.aux_operators_evaluated = aux_op_vals
162+
@staticmethod
163+
def _solve_dense(op_matrix: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
164+
if op_matrix.all() == op_matrix.conj().T.all():
165+
# Operator is Hermitian
166+
return np.linalg.eigh(op_matrix)
167+
else:
168+
return np.linalg.eig(op_matrix)
175169

176170
@staticmethod
177171
def _eval_aux_operators(
@@ -190,25 +184,42 @@ def _eval_aux_operators(
190184
else:
191185
values = {}
192186
key_op_iterator = aux_operators.items()
187+
193188
for key, operator in key_op_iterator:
194189
if operator is None:
195190
continue
196-
value = 0.0
191+
192+
if operator.num_qubits is None or operator.num_qubits < 1:
193+
logger.info(
194+
"The number of qubits of the %s operator must be greater than zero.", key
195+
)
196+
continue
197+
198+
op_matrix = None
197199
if isinstance(operator, PauliSumOp):
198200
if operator.coeff != 0:
199-
mat = operator.to_spmatrix()
200-
# Terra doesn't support sparse yet, so do the matmul directly if so
201-
# This is necessary for the particle_hole and other chemistry tests because the
202-
# pauli conversions are 2^12th large and will OOM error if not sparse.
203-
if isinstance(mat, scisparse.spmatrix):
204-
value = mat.dot(wavefn).dot(np.conj(wavefn))
205-
else:
206-
value = (
207-
Statevector(wavefn).expectation_value(operator.primitive)
208-
* operator.coeff
209-
)
201+
op_matrix = operator.to_spmatrix()
210202
else:
203+
try:
204+
op_matrix = operator.to_matrix(sparse=True)
205+
except TypeError:
206+
logger.debug(
207+
"WARNING: operator of type `%s` does not support sparse matrices. "
208+
"Trying dense computation",
209+
type(operator),
210+
)
211+
try:
212+
op_matrix = operator.to_matrix()
213+
except AttributeError as ex:
214+
raise AlgorithmError(f"Unsupported operator type {type(operator)}.") from ex
215+
216+
if isinstance(op_matrix, scisparse.csr_matrix):
217+
value = op_matrix.dot(wavefn).dot(np.conj(wavefn))
218+
elif isinstance(op_matrix, np.ndarray):
211219
value = Statevector(wavefn).expectation_value(operator)
220+
else:
221+
value = 0.0
222+
212223
value = value if np.abs(value) > threshold else 0.0
213224
# The value gets wrapped into a tuple: (mean, metadata).
214225
# The metadata includes variance (and, for other eigensolvers, shots).
@@ -225,8 +236,12 @@ def compute_eigenvalues(
225236

226237
super().compute_eigenvalues(operator, aux_operators)
227238

239+
if operator.num_qubits is None or operator.num_qubits < 1:
240+
raise AlgorithmError("The number of qubits of the operator must be greater than zero.")
241+
228242
self._check_set_k(operator)
229-
zero_op = PauliSumOp.from_list([("I", 1)]).tensorpower(operator.num_qubits) * 0.0
243+
244+
zero_op = SparsePauliOp(["I" * operator.num_qubits], coeffs=[0.0])
230245
if isinstance(aux_operators, list) and len(aux_operators) > 0:
231246
# For some reason Chemistry passes aux_ops with 0 qubits and paulis sometimes.
232247
aux_operators = [zero_op if op == 0 else op for op in aux_operators]
@@ -244,49 +259,51 @@ def compute_eigenvalues(
244259
# need to consider all elements if a filter is set
245260
self._k = 2**operator.num_qubits
246261

247-
self._ret = NumPyEigensolverResult()
248-
self._solve(operator)
262+
eigvals, eigvecs = self._solve(operator)
249263

250264
# compute energies before filtering, as this also evaluates the aux operators
251-
self._get_energies(operator, aux_operators)
265+
if aux_operators is not None:
266+
aux_op_vals = [
267+
self._eval_aux_operators(aux_operators, eigvecs[i]) for i in range(self._k)
268+
]
269+
else:
270+
aux_op_vals = None
252271

253272
# if a filter is set, loop over the given values and only keep
254273
if self._filter_criterion:
255-
256-
eigvecs = []
257-
eigvals = []
258-
aux_ops = []
259-
cnt = 0
260-
for i in range(len(self._ret.eigenvalues)):
261-
eigvec = self._ret.eigenstates[i]
262-
eigval = self._ret.eigenvalues[i]
263-
if self._ret.aux_operators_evaluated is not None:
264-
aux_op = self._ret.aux_operators_evaluated[i]
274+
filt_eigvals = []
275+
filt_eigvecs = []
276+
filt_aux_op_vals = []
277+
count = 0
278+
for i, (eigval, eigvec) in enumerate(zip(eigvals, eigvecs)):
279+
if aux_op_vals is not None:
280+
aux_op_val = aux_op_vals[i]
265281
else:
266-
aux_op = None
267-
if self._filter_criterion(eigvec, eigval, aux_op):
268-
cnt += 1
269-
eigvecs += [eigvec]
270-
eigvals += [eigval]
271-
if self._ret.aux_operators_evaluated is not None:
272-
aux_ops += [aux_op]
273-
if cnt == k_orig:
282+
aux_op_val = None
283+
284+
if self._filter_criterion(eigvec, eigval, aux_op_val):
285+
count += 1
286+
filt_eigvecs.append(eigvec)
287+
filt_eigvals.append(eigval)
288+
if aux_op_vals is not None:
289+
filt_aux_op_vals.append(aux_op_val)
290+
291+
if count == k_orig:
274292
break
275293

276-
self._ret.eigenstates = np.array(eigvecs)
277-
self._ret.eigenvalues = np.array(eigvals)
278-
# conversion to np.array breaks in case of aux_ops
279-
self._ret.aux_operators_evaluated = aux_ops
294+
eigvals = np.array(filt_eigvals)
295+
eigvecs = np.array(filt_eigvecs)
296+
aux_op_vals = filt_aux_op_vals
280297

281298
self._k = k_orig
282299

283-
# evaluate ground state after filtering (in case a filter is set)
284-
self._get_ground_state_energy(operator)
285-
if self._ret.eigenstates is not None:
286-
self._ret.eigenstates = [Statevector(vec) for vec in self._ret.eigenstates]
300+
result = NumPyEigensolverResult()
301+
result.eigenvalues = eigvals
302+
result.eigenstates = [Statevector(vec) for vec in eigvecs]
303+
result.aux_operators_evaluated = aux_op_vals
287304

288-
logger.debug("NumpyEigensolverResult:\n%s", self._ret)
289-
return self._ret
305+
logger.debug("NumpyEigensolverResult:\n%s", result)
306+
return result
290307

291308

292309
class NumPyEigensolverResult(EigensolverResult):

qiskit/algorithms/minimum_eigensolvers/numpy_minimum_eigensolver.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def __init__(
4343
) -> None:
4444
"""
4545
Args:
46-
filter_criterion: callable that allows to filter eigenvalues/eigenstates. The minimum
46+
filter_criterion: Callable that allows to filter eigenvalues/eigenstates. The minimum
4747
eigensolver is only searching over feasible states and returns an eigenstate that
4848
has the smallest eigenvalue among feasible states. The callable has the signature
4949
``filter(eigenstate, eigenvalue, aux_values)`` and must return a boolean to indicate

qiskit/quantum_info/operators/operator.py

+4
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,10 @@ def reverse_qargs(self):
470470
ret._op_shape = self._op_shape.reverse()
471471
return ret
472472

473+
def to_matrix(self):
474+
"""Convert operator to NumPy matrix."""
475+
return self.data
476+
473477
@classmethod
474478
def _einsum_matmul(cls, tensor, mat, indices, shift=0, right_mul=False):
475479
"""Perform a contraction using Numpy.einsum
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed an issue with
5+
:class:`~qiskit.algorithms.eigensolvers.NumPyEigensolver` and by extension
6+
:class:`~qiskit.algorithms.minimum_eigensolvers.NumPyMinimumEigensolver`
7+
where solving for
8+
:class:`~qiskit.quantum_info.operators.base_operator.BaseOperator`
9+
subclasses other than :class:`~qiskit.quantum_info.operators.Operator`
10+
would cause an error.

0 commit comments

Comments
 (0)