qDRIFT decomposition and error gate implementation

Hi!

I’m having issues with decomposing the qDRIFT template because the standard qml.transforms.decompose() function with a custom decomposition gate set doesn’t work. Moreover, I need to apply a noise model to this time evolution, but for that, I need the qDRIFT circuit decomposed into elementary gates. What should I do?

Hi @Alex1 , welcome to the Forum!

Can you please share a minimal reproducible code example showing the issues you’re having? I used the code below with no issues so I’m wondering if the problem happens only under specific circumstances.

import pennylane as qml

coeffs = [0.25, 0.75]
ops = [qml.X(0), qml.Z(0)]
H = qml.dot(coeffs, ops)

dev = qml.device("default.qubit", wires=2)

from functools import partial

@partial(qml.transforms.decompose, gate_set={qml.Toffoli, "RX", "RZ"})
@qml.qnode(dev)
def my_circ():
    # Prepare some state
    qml.Hadamard(0)

    # Evolve according to H
    qml.QDrift(H, time=1.2, n=10, seed=10)

    # Measure some quantity
    return qml.probs()

qml.draw_mpl(my_circ)()

Yes, if I consider simple Hamiltonians, no issues occour. But if I consider in the Hamiltonian even a little more complicated term like qml.X(0) @ qml.X(1), and I try to decompose the relative qDRIFT in terms of gate_set={“CZ”, “RX”, “RZ”, “X”, “SX”, “IsingZZ”}(the gate RZZ of qiskit should be IsingZZ in PennyLane, right?), the code still works, but with some problems.

Indeed, in the decomposition I should expect (from decomposition rules) 4 RZ, 2 RX and an IsingZZ, but in the figure I obtain 1 RX and 2 IsingZZ, with a warning indicates that CNOT does not define a decomposition and was not found in the target gate set (but CNOTs in this code should not be present in principle).

To clarify, I copied here your code with a change in the Hamiltonian and the warning ad.

import matplotlib.pyplot as plt
import pennylane as qml

coeffs = [1.0, 0.1]
ops = [qml.X(0) @ qml.X(1), qml.Z(0)]
H = qml.dot(coeffs, ops)

dev = qml.device("default.qubit", wires=2)

from functools import partial

@partial(qml.transforms.decompose, gate_set={"CZ", "RX", "RZ", "X", "SX", "IsingZZ"})
@qml.qnode(dev)
def my_circ():
    # Prepare some state
    #qml.Hadamard(0)

    # Evolve according to H
    qml.QDrift(H, time=1.2, n=1, seed=100)

    # Measure some quantity
    return qml.probs()

qml.draw_mpl(my_circ)()
fig, ax = qml.draw_mpl(my_circ)()
plt.show()

/home/neo-ale-pc/qenv/lib/python3.12/site-packages/pennylane/transforms/decompose.py:767: UserWarning: Operator CNOT does not define a decomposition and was not found in the target gate set. To remove this warning, add the operator name (CNOT) or type (<class ‘pennylane.ops.op_math.controlled_ops.CNOT’>) to the gate set.
warnings.warn(
/home/neo-ale-pc/qenv/lib/python3.12/site-packages/pennylane/transforms/decompose.py:767: UserWarning: Operator CNOT does not define a decomposition and was not found in the target gate set. To remove this warning, add the operator name (CNOT) or type (<class ‘pennylane.ops.op_math.controlled_ops.CNOT’>) to the gate set.
warnings.warn(

PS:In any case, the Hamiltonian I need to evolve in time and decompose (to add noise models to these elementary gates) is way more complicate, there are 180 terms, and each term is a Pauli string of 5/6 X,Y,Z gates, and the results in that case are completely wrong.

Hi @Alex1 ,

Thank you for adding this example. It does look like something strange is happening with qml.transforms.decompose. The code below, where I don’t specify the target gate set, works properly without the warning. Are you able to share the more complicated Hamiltonian that is giving the wrong results? Having this can help us properly understand and fix the issue.

import matplotlib.pyplot as plt
import pennylane as qml

coeffs = [1.0, 0.1]
ops = [qml.X(0) @ qml.X(1), qml.Z(0)]
H = qml.dot(coeffs, ops)

dev = qml.device("default.qubit", wires=2)

from functools import partial

@partial(qml.transforms.decompose)
@qml.qnode(dev)
def my_circ():
    # Prepare some state
    #qml.Hadamard(0)

    # Evolve according to H
    qml.QDrift(H, time=1.2, n=1, seed=100)

    # Measure some quantity
    return qml.probs()

qml.draw_mpl(my_circ, decimals=1)()

Thanks again for helping us improve.

Sure, I can share. As an overview, this is an Hubbard spinful Hamiltonian for a 3 x 2 square lattice (the coefficients are not still simplified) built starting from my personal fermion-to-qubit mapping (so not Jordan-Wigner or Bravyi-Kitaev).
However, reading the template documentation, I noticed a note advising that decomposition to qDRIFT with a costum gate set has been removed. It suggests to use apply() on all the operation in the decomposition, even if I don’t understand where I should insert apply() in the circuit properly.

0.5 * ((-1+0j) * (Y(1) @ X(3) @ Y(7) @ Y(10)) + (-1+0j) * (Z(0) @ X(1) @ X(3) @ Y(7) @ X(10))) + 0.5 * ((-1+0j) * (Y(0) @ X(1) @ Z(2) @ X(3) @ X(6) @ Y(7) @ Z(10)) + (1-0j) * (Y(0) @ X(1) @ Z(2) @ X(3) @ Y(6) @ X(7) @ Z(9))) + 0.5 * ((1+0j) * (X(1) @ Y(4)) + (-1+0j) * (Z(0) @ Y(1) @ X(4))) + 0.5 * ((-1+0j) * (Y(0) @ Y(1) @ Z(2) @ Z(4) @ X(6)) + (1-0j) * (Y(0) @ X(1) @ Z(2) @ Z(3) @ Y(6))) + 0.5 * ((1+0j) * (Y(0) @ Z(1) @ X(2) @ Y(5)) + (1-0j) * (X(0) @ X(2) @ X(5))) + 0.5 * ((1+0j) * (X(2) @ X(6)) + (1-0j) * (Y(2) @ Z(5) @ Y(6))) + 0.5 * ((1+0j) * (Y(0) @ Y(1) @ Z(2) @ X(4) @ Z(6) @ X(8) @ Y(11)) + (-1+0j) * (Y(0) @ Y(1) @ Z(2) @ Y(4) @ Z(6) @ X(8) @ X(11))) + 0.5 * ((1+0j) * (Y(0) @ X(1) @ Z(2) @ Z(3) @ Z(6) @ Y(8)) + (-1+0j) * (Y(0) @ Y(1) @ Z(2) @ Z(4) @ Z(6) @ X(8) @ Z(11))) + 0.5 * ((1+0j) * (Z(1) @ Y(3) @ X(4)) + (-1+0j) * (Z(1) @ X(3) @ Y(4) @ Z(7))) + 0.5 * ((-1+0j) * (Y(3) @ X(7) @ Y(9)) + (-1+0j) * (Z(1) @ X(3) @ Z(4) @ X(7) @ X(9) @ Z(13))) + 0.5 * ((-1+0j) * (Y(0) @ X(1) @ X(2) @ Y(3) @ X(5)) + (1-0j) * (Y(0) @ X(1) @ X(2) @ X(3) @ Y(5) @ Z(7))) + 0.5 * ((-1+0j) * (Y(0) @ X(1) @ X(2) @ X(3) @ Z(5) @ X(7) @ Y(9)) + (1-0j) * (Y(0) @ X(1) @ Y(2) @ X(3) @ X(7) @ X(9) @ Z(13))) + 0.5 * ((-1+0j) * (Y(0) @ X(1) @ X(2) @ X(3) @ X(5) @ Y(7) @ Y(10)) + (1-0j) * (Y(0) @ X(1) @ X(2) @ X(3) @ Y(5) @ Y(7) @ X(10))) + 0.5 * ((-1+0j) * (Y(0) @ X(1) @ X(2) @ X(3) @ Z(5) @ Y(7) @ Z(10)) + (1-0j) * (Y(0) @ X(1) @ Y(2) @ X(3) @ X(7) @ Z(9))) + 0.5 * ((1+0j) * (Y(0) @ X(1) @ Z(2) @ X(3) @ Z(6) @ Z(7) @ X(8) @ Y(11)) + (-1+0j) * (Y(0) @ X(1) @ Z(2) @ Y(3) @ Z(6) @ X(8) @ X(11))) + 0.5 * ((1+0j) * (Y(0) @ X(1) @ Z(2) @ X(3) @ Z(6) @ X(7) @ Y(8) @ X(9) @ Z(13)) + (-1+0j) * (Y(0) @ X(1) @ Z(2) @ X(3) @ Z(6) @ X(7) @ X(8) @ Y(9) @ Z(11))) + 0.5 * ((1+0j) * (Y(0) @ X(1) @ Z(2) @ X(3) @ Z(6) @ Y(7) @ X(8) @ X(10) @ Y(11)) + (-1+0j) * (Y(0) @ X(1) @ Z(2) @ X(3) @ Z(6) @ Y(7) @ X(8) @ Y(10) @ X(11))) + 0.5 * ((1+0j) * (Y(0) @ X(1) @ Z(2) @ X(3) @ Z(6) @ X(7) @ Y(8) @ Z(9)) + (-1+0j) * (Y(0) @ X(1) @ Z(2) @ X(3) @ Z(6) @ Y(7) @ X(8) @ Z(10) @ Z(11))) + 0.25 * ((-1+0j) * (Z(0) @ Z(1)) + (-1+0j) * Z(6) + (1-0j) * (Z(0) @ Z(1) @ Z(6))) + 0.25 * ((-1+0j) * Z(4) + (-1+0j) * (Z(1) @ Z(3) @ Z(4)) + (1-0j) * (Z(1) @ Z(3))) + 0.25 * ((-1+0j) * Z(5) + (-1+0j) * (Z(2) @ Z(5)) + (1-0j) * Z(2)) + 0.25 * ((-1+0j) * (Z(3) @ Z(7)) + (-1+0j) * (Z(9) @ Z(13)) + (1-0j) * (Z(3) @ Z(7) @ Z(9) @ Z(13))) + 0.25 * ((-1+0j) * Z(10) + (-1+0j) * (Z(7) @ Z(9) @ Z(10)) + (1-0j) * (Z(7) @ Z(9))) + 0.25 * ((-1+0j) * Z(11) + (-1+0j) * (Z(8) @ Z(11)) + (1-0j) * Z(8)) .

Anyway in the meanwhile, if you are interested I built from zero another code where I integrated qDRIFT algorithm with the correct gate count in terms of these costum elementary gates (if you notice these are the native gates of heavy-hex IBM architectures), which also properly decomposes qDRIFT gates if you want construct a realistic noise model.

If we use multiple decompose transforms together with max_expansion=1 , we can step through how the decomposition is occurring to get a better idea of what is going on:

gate_set = {"CZ", "RX", "RZ", "X", "SX", "IsingZZ"}

for i in range(3):
    print("\nnumber of expansions: ", i)
    my_circ = qml.transforms.decompose(my_circ, gate_set=gate_set, max_expansion=1)
    print(qml.draw(my_circ, level="user")())
number of expansions:  0
0: ─╭Exp(1.32j X@X)─┤  Probs
1: ─╰Exp(1.32j X@X)─┤  Probs

number of expansions:  1
0: ─╭IsingXX(-2.64)─┤  Probs
1: ─╰IsingXX(-2.64)─┤  Probs

number of expansions:  2
0: ─╭●──RX(-2.64)─╭●─┤  Probs
1: ─╰X────────────╰X─┤  Probs

We can here see that we first decompose to an en evolution of X@X, then decompose that to the IsingXX, and then decompose that to RX and CNOT. We stop at RX because it is in the gateset, and we stop at CNOT because we it doesn’t have a decomposition in our default system to prevent infinite decomposition loops.

We do have a new feature we are currently working on making default that may be useful for you called “graph decompositions”. This allows multiple decompositions, or fixed alternate decompositions to be provided for operators. This will allow you to override which decompositions you want to use. There is a section in the decompose transform documentation that goes over this. In particular, you may be interested in the “Customizing decompositions” section. This will allow you to specify another decomposition for gates like IsingXX and PauliRot that the exponential will often decompose too.

1 Like

Hi @Alex1 ,

Thanks for sharing your Hamiltonian. Do you have any other examples with smaller Hamiltonians where the decomposition is not what you expected? If so, can you share the Hamiltonian, the decomposition provided by PennyLane, and the expected decomposition?

Regarding your question about apply(), I agree that it’s confusing as it’s written in the documentation at the moment. I’ve noted this down as something we should fix! Here’s a clear explanation provided by Christina:

There was a time you could do something like:

decomp = [qml.X(0), qml.Y(0)] 
op = QDrift(X(0) + Y(0), 0.5, decomposition=decomp)

and then it would decompose with decomp. Now we’re of the opinion if you don’t want to use QDrift’s decomposition, then you shouldn’t use QDrift. Instead, you should queue the operations, potentially just like:

decomp = [qml.X(0), qml.Y(0)] 
@qml.qnode(dev) 
def c(): 
    [qml.apply(op) for op in decomp]

Regarding my previous post, I wanted to clarify that I was wrong when I mentioned that something strange was happening. I mixed up IsingXX and IsingZZ. The reason why the code with no target gate set worked well is because it decomposed it to IsingXX. But since IsingXX wasn’t in your target gate set, then PennyLane tried to decompose it further, reaching one final expansion as shown by Christina.

Given that you expected a different decomposition for the Hamiltonian below with QDrift, can you please share a circuit diagram of the decomposition you expected to have? I want to check that it is indeed correct.

coeffs = [1.0, 0.1]
ops = [qml.X(0) @ qml.X(1), qml.Z(0)]
H = qml.dot(coeffs, ops)

Finally, thank you @christina for sharing these details and the info on the graph decompositions! @Alex1, let us know if you find these decompositions useful.

Hi again!
Unfortunately, I don’t have issues with simpler hamiltonians because my simulation involves bigger lattices and terms in hamiltonians increase! This was the simplest example I can provide.

Talking about your previous post, I dont’t still understand why, given my costum set, it does not decompose exactly the template hamiltonian I wrote. Indeed, the set in the code should exactly decompose a generic hamiltonian expressed in terms of Pauli strings (it is mathematically proven), and in any case, CNOT shold not appear. To clarify, this set is also the native set of gates that IBM has built in their QPU’s, and given a generic hamiltonian the decomposition provided by the transpiler is exact.

Repling to Christina about the last sentence, I think it could be very helpful to introduce a function that, given a generic circuit, decomposes that circuit into a custom set of gates. The circuit decomposed, could be applied with a noise associated to each gate in the set. A feature like this, could be useful in extimating resources if someone need to do further calculations on a real QPU (without wasting runtime on the QPU).
Anyway, thank you very much for your time!

Hi @Alex1 ,

I understand what you mean. I think the issue here is in using QDrift (which doesn’t let you set a custom decomposition) vs adding the gates in a different way. I’ll code up an example to see if I can show the difference.

Oh, no no ok I understand the point. The CNOTs derive from another decomposition rule, different from that I set. But in that decomposition there are not the required gates to decompose the circuit exactly, and the program applies CNOTs with a warning, advising that the CNOTs are not in the decomposition rule.
Anyway, if you want, I’d appreciate the example to spot the difference.

Yes, exactly. You got it @Alex1 !
I’ll still try to code up an example to add it here for clarity, both for you and others who might run into this issue in the future!