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

Parameter handling in SparsePauliOp #7215

Closed
wants to merge 12 commits into from
6 changes: 6 additions & 0 deletions qiskit/circuit/gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,5 +237,11 @@ def validate_parameter(self, parameter):
3,
)
return parameter
elif isinstance(parameter, complex):
if np.isclose(np.imag(parameter), 0):
return np.real(parameter)
Comment on lines +240 to +242
Copy link
Member

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, and complex should raise the "unsupported type" error even if it's like (1 + 0j).

else:
msg = f"Bound parameter is complex in gate {self.name}"
raise CircuitError(msg)
else:
raise CircuitError(f"Invalid param type {type(parameter)} for gate {self.name}.")
2 changes: 1 addition & 1 deletion qiskit/opflow/evolutions/trotterizations/qdrift.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

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

How come this is necessary? Numpy would do this automatically if weights/lambd isn't an array or if it's of dtype int or something. If the dtype may be complex, then this will emit a warning, and the surrounding code presumably has a bug.

)

return ComposedOp(sampled_ops).reduce()
19 changes: 17 additions & 2 deletions qiskit/opflow/primitive_ops/pauli_sum_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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 if block.

Comment on lines +366 to +369
Copy link
Member

Choose a reason for hiding this comment

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

You don't need to compare against Parameter and ParameterExpression - only ParameterExpression. That's because Parameter inherits from ParameterExpression.

Also, when you've got if/else statements whose branches are basically the same length, it's a bit easier to read if you don't negate the condition. For example here, I have to parse it in my head as "if not this, ..., else that", so when I'm thinking about the "else" condition, there's a double negative ("not not this").

return SummedOp(
[
PauliOp(pauli, to_native(coeff))
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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 False.

return np.all(is_real) and np.all(self.primitive.paulis.phase == 0)
Comment on lines +454 to +460
Copy link
Member

Choose a reason for hiding this comment

The 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 else branch necessary?

35 changes: 25 additions & 10 deletions qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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 False, and you don't need to remember any True results.


return super().__eq__(other) and np.all(close_coeffs) and self.paulis == other.paulis

@property
def settings(self) -> Dict:
Expand Down Expand Up @@ -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
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 going to have a really big impact on performance, even for coefficient arrays that have dtype=complex. We need to maintain all the fastest paths in the case that this new functionality isn't being used.

# 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):
Expand Down