Qiskit / qiskit

Qiskit is an open-source SDK for working with quantum computers at the level of extended quantum circuits, operators, and primitives.
https://www.ibm.com/quantum/qiskit
Apache License 2.0
4.85k stars 2.29k forks source link

Support "direct" `Instruction -> SparsePauliOp` conversion #11892

Open mrossinek opened 4 months ago

mrossinek commented 4 months ago

What should we add?

I have recently found myself having to convert an Instruction object into a SparsePauliOp. This is currently possible via:

from qiskit.quantum_info import Operator, SparsePauliOp

operator = SparsePauliOp.from_operator(Operator(instruction))

While this works, internally this will convert the instruction to a matrix and subsequently decompose that Matrix into Pauli terms. Another limitation is that this does not work for parameterized instructions.

Thus, I would like to propose a new method be added to the gate objects (not sure which exact class this would end up on). This method should:

As an initial naming suggestion I propose to_symbolic (or something along these lines), to indicate that this can handle parameters. At the same time this also hints at the "symbolic" form in terms of Paulis.

The example at the top could be used as a fallback implementation which would gracefully handle encountering a parameter.


For this method to be truly useful, we might need #11891 in order to retain the gate qubit indices as part of the SparsePauliOp. But I think an initial implementation can already be done without this logic.


I am happy to contribute a PR for this. I would require minimal guidance on the interplay of the different gate classes (e.g. Gate, Instruction, etc.).

jakelishman commented 4 months ago

I think a from_instruction constructor for all the Pauli-like operators in quantum_info is a fine direction to go, and in line with other things we have there.

All existing from_instruction-type syntheses (see Clifford.from_instruction and basically everything in qiskit.synthesis) do this by methods that inspect the instructions and dispatch on them, and I'd suggest that that's a more consistent way to go rather than adding more magic decomposition methods to Instruction and Gate, which would be interface liabilities for us to change the internal representations of them. We might want to improve the way that user-defined classes hook into these in the future, but I think it's probably best to do that in bulk, if/when we look at it. It also rather simplifies the naming of the method: Pauli.from_instruction, SparsePauliOp.from_instruction and Clifford.from_instruction all are absolutely clear what they do, but should XGate.to_symbolic return a Pauli, andSparsePauliOp or a Clifford? It's also easier to add new bits to the interface (new keyword arguments, etc) if it's a "from" method on a class we entirely control than a "to" method on classes that are intended to be subclassed.

jakelishman commented 4 months ago

Actually, having just written that, two more thoughts:

mrossinek commented 4 months ago

Thanks for the input! I will do some more reading up on those existing paths. From what I see, however, these also do not currently support parameterized Instruction objects.

jakelishman commented 4 months ago

In what situations are you expecting to have a parametric Instruction object that can be converted to SparsePauliOp? The Clifford methods have some ways of checking if a value for a rotation gate is a multiple of $\pi/2$, so if that's all you need, then those can potentially be re-used for the Pauli constructors.

mrossinek commented 4 months ago

I can see a use-case where one might want to convert between a circuit operation (i.e. Gate or Instruction) and an operator in Pauli form. Since we have the ability to represent parameterized SparsePauliOp objects, I don't think it is too far fetched that this can also be of interest. Here is a very simple example for the case of a single parameterized RXGate:

from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.quantum_info import Operator, SparsePauliOp

qc = QuantumCircuit(1)
qc.rx(1.57, 0)

rx_instruction = qc[0].operation
print(rx_instruction)
sop = SparsePauliOp.from_operator(Operator(rx_instruction))
print(sop)

a = Parameter("a")

qc_p = QuantumCircuit(1)
qc_p.rx(1.57 * a, 0)

rx_instruction_p = qc_p[0].operation
print(rx_instruction_p)
print(a * sop)

This outputs:

Instruction(name='rx', num_qubits=1, num_clbits=0, params=[1.57])
SparsePauliOp(['I', 'X'],
              coeffs=[0.70738827+0.j        , 0.        -0.70682518j])
Instruction(name='rx', num_qubits=1, num_clbits=0, params=[ParameterExpression(1.57*a)])
SparsePauliOp(['I', 'X'],
              coeffs=[ParameterExpression(0.7073882691672*a),
 ParameterExpression(-0.706825181105366*I*a)])

Trying sop = SparsePauliOp.from_operator(Operator(rx_instruction_p)) will fail because the unbound parameter a cannot be cast to a float:

Traceback (most recent call last):
  File "/home/oss/Files/Dev/Qiskit/qiskit/main/tmp-sparse-pauli-gate.py", line 21, in <module>
    sop_p = SparsePauliOp.from_operator(Operator(rx_instruction_p))
  File "/home/oss/Files/Dev/Qiskit/qiskit/main/qiskit/quantum_info/operators/operator.py", line 97, in __init__
    self._data = self._init_instruction(data).data
  File "/home/oss/Files/Dev/Qiskit/qiskit/main/qiskit/quantum_info/operators/operator.py", line 700, in _init_instruction
    return Operator(np.array(instruction, dtype=complex))
  File "/home/oss/Files/Dev/Qiskit/qiskit/main/qiskit/circuit/library/standard_gates/rx.py", line 125, in __array__
    cos = math.cos(self.params[0] / 2)
  File "/home/oss/Files/Dev/Qiskit/qiskit/main/qiskit/circuit/parameterexpression.py", line 415, in __float__
    raise TypeError(
TypeError: ParameterExpression with unbound parameters (dict_keys([Parameter(a)])) cannot be cast to a float.

I am aware that my proposal has quite some caveats and I also understand that a user may be facing a possibly exponential blow-up of parameter expressions when doing the above repeatedly and composing the results. Nonetheless, I think that a "symbolic" interpretation of a gate in terms of Pauli terms can have value. By that I mean a way to convert standard gates such as RX(a) to a form like this: cos(a / 2) * I - i * sin(a / 2) * X.