-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Parameter handling in SparsePauliOp #7215
Changes from all commits
4e6c1c3
b48e411
e47f856
7d922bb
fd4fcc6
43f8ce0
09cf891
fe4c2f3
9c73918
3ea8a9d
8cec777
4dd2e02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -73,7 +73,7 @@ def convert(self, operator: OperatorBase) -> OperatorBase: | |
# and multiplication by a constant factor. | ||
scaled_ops = [(op * (factor / op.coeff)).exp_i() for op in operator_iter] | ||
sampled_ops = algorithm_globals.random.choice( | ||
scaled_ops, size=(int(N * self.reps),), p=weights / lambd | ||
scaled_ops, size=(int(N * self.reps),), p=np.array(weights / lambd, dtype=float) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How come this is necessary? Numpy would do this automatically if |
||
) | ||
|
||
return ComposedOp(sampled_ops).reduce() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,7 @@ | |
import numpy as np | ||
from scipy.sparse import spmatrix | ||
|
||
from qiskit.circuit import Instruction, ParameterExpression | ||
from qiskit.circuit import Instruction, Parameter, ParameterExpression | ||
from qiskit.opflow.exceptions import OpflowError | ||
from qiskit.opflow.list_ops.summed_op import SummedOp | ||
from qiskit.opflow.list_ops.tensored_op import TensoredOp | ||
|
@@ -358,6 +358,15 @@ def to_native(x): | |
to_native(np.real_if_close(self.primitive.coeffs[0])) * self.coeff, | ||
) | ||
coeffs = np.real_if_close(self.primitive.coeffs) | ||
if not self.primitive.coeffs.dtype == object: | ||
coeffs = np.real_if_close(self.primitive.coeffs) | ||
else: | ||
coeffs = [] | ||
for coeff in self.primitive.coeffs: | ||
if not isinstance(coeff, (Parameter, ParameterExpression)): | ||
coeffs.append(np.real_if_close(coeff).item()) | ||
else: | ||
coeffs.append(coeff) | ||
Comment on lines
360
to
+369
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There looks like some duplication here between the unchanged line above this, and the new
Comment on lines
+366
to
+369
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need to compare against Also, when you've got |
||
return SummedOp( | ||
[ | ||
PauliOp(pauli, to_native(coeff)) | ||
|
@@ -442,4 +451,10 @@ def is_zero(self) -> bool: | |
return op.coeff == 1 and len(op) == 1 and primitive.coeffs[0] == 0 | ||
|
||
def is_hermitian(self): | ||
return np.isreal(self.coeffs).all() and np.all(self.primitive.paulis.phase == 0) | ||
if not self.coeffs.dtype == object: | ||
return np.isreal(self.coeffs).all() and np.all(self.primitive.paulis.phase == 0) | ||
else: | ||
is_real = [] | ||
for coeff in self.coeffs: | ||
is_real.append(np.isreal(coeff)) | ||
Comment on lines
+457
to
+459
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no need to build up a list here - if any of them are false as you're looping through, you can immediately return |
||
return np.all(is_real) and np.all(self.primitive.paulis.phase == 0) | ||
Comment on lines
+454
to
+460
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The two paths look like they're doing identical things here. Is the |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ | |
from qiskit.quantum_info.operators.symplectic.pauli_table import PauliTable | ||
from qiskit.quantum_info.operators.symplectic.pauli_utils import pauli_basis | ||
from qiskit.utils.deprecation import deprecate_function | ||
from qiskit.circuit import Parameter, ParameterExpression | ||
|
||
|
||
class SparsePauliOp(LinearOp): | ||
|
@@ -88,7 +89,12 @@ def __init__(self, data, coeffs=None, *, ignore_pauli_phase=False, copy=True): | |
if coeffs is None: | ||
coeffs = np.ones(pauli_list.size, dtype=complex) | ||
else: | ||
coeffs = np.array(coeffs, copy=copy, dtype=complex) | ||
try: | ||
coeffs = np.array(coeffs, copy=copy, dtype=complex) | ||
except TypeError: | ||
# Initialize as array of objects if there are parameters. | ||
# This is generally avoided since it makes numpy slower. | ||
coeffs = np.array(coeffs, copy=copy, dtype=object) | ||
|
||
if ignore_pauli_phase: | ||
# Fast path used in copy operations, where the phase of the PauliList is already known | ||
|
@@ -99,7 +105,7 @@ def __init__(self, data, coeffs=None, *, ignore_pauli_phase=False, copy=True): | |
else: | ||
# move the phase of `pauli_list` to `self._coeffs` | ||
phase = pauli_list.phase | ||
self._coeffs = np.asarray((-1j) ** phase * coeffs, dtype=complex) | ||
self._coeffs = np.asarray((-1j) ** phase * coeffs, dtype=coeffs.dtype) | ||
pauli_list._phase = np.mod(pauli_list._phase - phase, 4) | ||
self._pauli_list = pauli_list | ||
|
||
|
@@ -128,11 +134,15 @@ def __repr__(self): | |
|
||
def __eq__(self, other): | ||
"""Check if two SparsePauliOp operators are equal""" | ||
return ( | ||
super().__eq__(other) | ||
and np.allclose(self.coeffs, other.coeffs) | ||
and self.paulis == other.paulis | ||
) | ||
close_coeffs = [] | ||
for i in range(self.coeffs.shape[0]): | ||
# Check for Parameters separately | ||
if isinstance(self.coeffs[i], ParameterExpression): | ||
close_coeffs.append(self._coeffs[i] == other._coeffs[i]) | ||
else: | ||
close_coeffs.append(np.isclose(self.coeffs[i], other.coeffs[i])) | ||
Comment on lines
+137
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no need to build up a list here - as soon as you find any element that's |
||
|
||
return super().__eq__(other) and np.all(close_coeffs) and self.paulis == other.paulis | ||
|
||
@property | ||
def settings(self) -> Dict: | ||
|
@@ -388,10 +398,15 @@ def simplify(self, atol=None, rtol=None): | |
# Pack bool vectors into np.uint8 vectors by np.packbits | ||
array = np.packbits(self.paulis.x, axis=1) * 256 + np.packbits(self.paulis.z, axis=1) | ||
_, indexes, inverses = np.unique(array, return_index=True, return_inverse=True, axis=0) | ||
coeffs = np.zeros(indexes.shape[0], dtype=complex) | ||
coeffs = np.zeros(indexes.shape[0], dtype=self.coeffs.dtype) | ||
np.add.at(coeffs, inverses, self.coeffs) | ||
# Delete zero coefficient rows | ||
is_zero = np.isclose(coeffs, 0, atol=atol, rtol=rtol) | ||
# Delete zero coefficient rows (Ignore if dealing with Parameters) | ||
is_zero = [] | ||
for coeff in coeffs: | ||
if isinstance(coeff, (Parameter, ParameterExpression)): | ||
is_zero.append(False) | ||
else: | ||
is_zero.append(np.isclose(coeff, 0, atol=atol, rtol=rtol)) | ||
Comment on lines
-393
to
+409
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is going to have a really big impact on performance, even for coefficient arrays that have |
||
# Check edge case that we deleted all Paulis | ||
# In this case we return an identity Pauli with a zero coefficient | ||
if np.all(is_zero): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems dangerous, and I'm not a huge fan. The tolerance for conversion to
real
is implicit, so if your parameters are all small values, then this would silently fail on any small complex. I think the code that actually wants to set the parameter should be responsible for this conversion, if they want it, andcomplex
should raise the "unsupported type" error even if it's like(1 + 0j)
.