qiskit icon indicating copy to clipboard operation
qiskit copied to clipboard

`SparsePauliOp.from_sparse_list` should handle parameterized coefficients

Open Cryoris opened this issue 1 month ago • 1 comments

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

Cryoris avatar Dec 08 '25 09:12 Cryoris

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.

RajdeepAher avatar Dec 08 '25 13:12 RajdeepAher