`SparsePauliOp.from_sparse_list` should handle parameterized coefficients
Environment
-
Qiskit version: any that has
SparsePauliOp.from_sparse_list - Python version: 3.11
- Operating system: macOS
What is happening?
From @BryceFuller:
Another question, what is the proper way to instantiate a SparsePauliOp with parameterized coefficients? I'm running into a curious snag where I have an already parameterized operator, which I then want to apply transformations to. I'm exporting to a sparse list, making some changes, then trying to re-instantiate the operator, but I observe an error with the parameterized coefficients.
How can we reproduce the issue?
Here's a MWE
param = Parameter('a')
# In my situation, this parameterized operator is given to me after being
# made through composition of smaller parameterized operators
op1 = SparsePauliOp.from_sparse_list([('IX', [0,1], 2),('ZI', [0,1], 3)], num_qubits = 2)
op1 *= param
# This breaks because there are parameter expressions in the coeffs
op2 = SparsePauliOp.from_sparse_list(op1.to_sparse_list(), num_qubits=2)
What should happen?
Like the SparsePauliOp initializer, we should determine the dtype from the input coefficients. The dtype can already be set manually, via
op2 = SparsePauliOp.from_sparse_list(op1.to_sparse_list(), num_qubits=2, dtype=object)
but it would be nice to automate this, given that the class initializer does the same thing.
Any suggestions?
No response
Hello, here is my write-up for this issue:
The SparsePauliOp checks the dtype in following way during initialization:
class SparsePauliOp(LinearOp):
def __init__(
self,
data: PauliList | SparsePauliOp | Pauli | list | str,
coeffs: np.ndarray | None = None,
*,
ignore_pauli_phase: bool = False,
copy: bool = True,
):
if coeffs is None:
coeffs = np.ones(pauli_list.size, dtype=complex)
else:
if isinstance(coeffs, np.ndarray):
dtype = object if coeffs.dtype == object else complex
else:
if not isinstance(coeffs, Sequence):
coeffs = [coeffs]
if any(isinstance(coeff, ParameterExpression) for coeff in coeffs):
dtype = object
else:
dtype = complex
whereas from_sparse_list method does:
@staticmethod
def from_sparse_list(
obj: Iterable[tuple[str, list[int], complex]],
num_qubits: int,
do_checks: bool = True,
dtype: type = complex, #<-- this is the problem
) -> SparsePauliOp:
obj = list(obj) # To convert zip or other iterable
size = len(obj)
if size == 0:
obj = [("I" * num_qubits, range(num_qubits), 0)]
size = len(obj)
coeffs = np.zeros(size, dtype=dtype) #<-- this is the problem
labels = np.zeros(size, dtype=f"<U{num_qubits}")
we can keep the signature as dtype: type | None = None instead and then do perform these checks instead
if dtype is None:
coeffs_list = [coeff for (_, _, coeff) in obj]
if any(isinstance(coeff, ParameterExpression) for coeff in coeffs_list):
dtype = object
else:
dtype = complex
coeffs = np.zeros(size, dtype=dtype)
now running:
from qiskit.circuit.parameter import Parameter
from qiskit.quantum_info.operators.symplectic.sparse_pauli_op import SparsePauliOp
param = Parameter('a')
# In my situation, this parameterized operator is given to me after being
# made through composition of smaller parameterized operators
op1 = SparsePauliOp.from_sparse_list([('IX', [0,1], 2),('ZI', [0,1], 3)], num_qubits = 2)
op1 *= param
print(op1)
# This breaks because there are parameter expressions in the coeffs
op2 = SparsePauliOp.from_sparse_list(op1.to_sparse_list(), num_qubits=2)
print(op2)
returns
SparsePauliOp(['XI', 'IZ'],
coeffs=[<qiskit._accelerate.circuit.ParameterExpression object at 0x78bf4e6c4c90>,
<qiskit._accelerate.circuit.ParameterExpression object at 0x78bf4e6c4e10>])
SparsePauliOp(['XI', 'IZ'],
coeffs=[<qiskit._accelerate.circuit.ParameterExpression object at 0x78bf4e6c50b0>,
<qiskit._accelerate.circuit.ParameterExpression object at 0x78bf4e6c5110>])
instead of an error.
Note that, I feel, similar changes should be made from_list too.