How to extend CVOperation to support parameter shift for given circuit

I am trying to train a parametrized quantum circuit with gradients calculated using parameter shift. I am trying to use the following class in my run_circuit() function (which has the qnode decorator) but it doesn’t work.

class Loader(Operation):
    num_params = 1
    num_wires = 2
    par_domain = "R"

    grad_method = "A"  # Analytic differentiation with Parameter shift
    grad_recipe = None

    @staticmethod
    def compute_decomposition(theta: float, wires: int | list):
      qml.RY(theta, wires=0)
      qml.RY(theta, wires=1)
      qml.IsingXX(theta, wires=[0, 1])
      qml.RY(theta, wires=0)
      qml.RY(theta, wires=1)

Moreover, here is the run circuit function:

@qml.qnode(dev, diff_method = "parameter-shift")
def run_circuit(phi, weight0, weight1, weight2, weight3, weight4, weight5):
    """
    Input a numpy array feature (which encodes a single normalized angle encoded spot price)
    """
    loader.Loader(phi, wires=[0,1])
    qml.StronglyEntanglingLayers(np.tensor([[[weight0, weight1, weight2] , [weight3, weight4, weight5]]]), wires=[0,1])
    return qml.expval(qml.PauliZ(0))

It works as expected when I modify as such:

@qml.qnode(dev, diff_method = "parameter-shift")
def run_circuit(phi, weight0, weight1, weight2, weight3, weight4, weight5):
    """
    Input a numpy array feature (which encodes a single normalized angle encoded spot price)
    """
    qml.RY(theta, wires=0)
    qml.RY(theta, wires=1)
    qml.IsingXX(theta, wires=[0, 1])
    qml.RY(theta, wires=0)
    qml.RY(theta, wires=1)
    qml.StronglyEntanglingLayers(np.tensor([[[weight0, weight1, weight2] , [weight3, weight4, weight5]]]), wires=[0,1])
    return qml.expval(qml.PauliZ(0))

Hey @Nikhil_Narayanan! Welcome to the forum :rocket:

In v0.22 of PennyLane, there were some changes to how one can make custom operations:

  • No two-term parameter-shift rule is assumed anymore by default. (#2227). Previously, operations marked for analytic differentiation that did not provide a generator, parameter_frequencies or a custom grad_recipe were assumed to satisfy the two-term shift rule. This now has to be made explicit for custom operations by adding any of the above attributes.

Here’s a simpler and complete example using your operator.

import pennylane as qml
from pennylane import numpy as np

class Loader(qml.operation.Operation):
    num_params = 1
    num_wires = 2
    par_domain = "R"

    grad_method = "A"  # Analytic differentiation with Parameter shift
    grad_recipe = None

    @staticmethod
    def compute_decomposition(theta, wires):
      qml.RY(theta, wires=wires[0])
      qml.RY(theta, wires=wires[1])
      qml.IsingXX(theta, wires=wires)
      qml.RY(theta, wires=wires[0])
      qml.RY(theta, wires=wires[1])

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

@qml.qnode(dev, diff_method = "parameter-shift")
def run_circuit(phi):
    Loader(phi, wires=[0, 1])
    return qml.expval(qml.PauliX(0))

theta = np.array(0.1, requires_grad=True)
print(qml.grad(run_circuit)(theta))

'''
OperatorPropertyUndefined: The operation Loader does not have a grad_recipe, parameter_frequencies or a generator defined. No parameter shift rule can be applied.
'''

When I try to use your operator with v0.30 (the latest version), I get the error above :point_up:. So, you need to define one of:

  • grad_recipe(see here for an example) ,
  • parameter_frequencies (see here for an example), or
  • generator (see here for an example),

in order for it to work. Hope this helps!

Hi Isaac, thank you very much for your reply! I tried adding the following line to my class:

grad_recipe = ([[0.5, 1, pi / 2], [-0.5, 1, -pi / 2]],)

While the code does run fine, when computing the gradient, it seems that all the values are 0 (this is not the case when I try using the prebuilt templates from pennylane. Could you help me find where I can understand what exactly the grad_recipe/generator/parameter_frequencies are? I don’t think I understand what they wrote in the documentation (as to why the gradients are becoming 0 for me).

Glad this helped a bit! I tried running this and the gradients aren’t zero for me :thinking:

import pennylane as qml
from pennylane import numpy as np

class Loader(qml.operation.Operation):
    num_params = 1
    num_wires = 2
    par_domain = "R"

    grad_method = "A"  # Analytic differentiation with Parameter shift
    grad_recipe = ([[0.5, 1, np.pi / 2], [-0.5, 1, -np.pi / 2]],)

    @staticmethod
    def compute_decomposition(theta, wires):
      qml.RY(theta, wires=wires[0])
      qml.RY(theta, wires=wires[1])
      qml.IsingXX(theta, wires=wires)
      qml.RY(theta, wires=wires[0])
      qml.RY(theta, wires=wires[1])

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

@qml.qnode(dev, diff_method = "parameter-shift")
def run_circuit(phi):
    """
    Input a numpy array feature (which encodes a single normalized angle encoded spot price)
    """
    Loader(phi, wires=[0, 1])
    return qml.expval(qml.PauliX(0))

theta = np.array(0.1, requires_grad=True)

opt = qml.GradientDescentOptimizer(0.1)

for _ in range(10):
    theta = opt.step(run_circuit, theta)
    print(qml.grad(run_circuit)(theta), theta)

'''
0.009722806259971711 0.09900830809618949
0.009534309646146294 0.09803602747019231
0.009351215857823753 0.09708259650557768
0.00917332152676964 0.0961474749197953
0.009000432705141143 0.09523014276711834
0.008832364350146032 0.09433009949660423
0.008668939841056184 0.09344686306158963
0.008509990526297917 0.092579969077484
0.00835535529850992 0.09172897002485421
0.008204880195630204 0.09089343449500321
'''

Could you help me find where I can understand what exactly the grad_recipe/generator/parameter_frequencies are?

Great question! I recommend going through some of our content to get a better understanding of how the parameter-shift rule works:

Let me know if these help!

I got the following code to work; but have a style question, what is the recommended way to deal with multiple parameters when using Loader classes - it is quite clunky to use arrays of pennylane.numpy tensors for example

import pennylane as qml
from pennylane import numpy as np

class Loader(qml.operation.Operation):
    num_params = 2
    num_wires = 2
    par_domain = "R"

    grad_method = "A"  # Analytic differentiation with Parameter shift
    grad_recipe = ([[0.5, 1, np.pi / 2], [-0.5, 1, -np.pi / 2]],[[0.5, 1, np.pi / 2], [-0.5, 1, -np.pi / 2]],)

    @staticmethod
    def compute_decomposition(theta1,theta2, wires):
      qml.RY(theta1, wires=wires[0])
      qml.RY(theta1, wires=wires[1])
      qml.IsingXX(theta2, wires=wires)
      qml.RY(theta1, wires=wires[0])
      qml.RY(theta1, wires=wires[1])

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

@qml.qnode(dev, diff_method = "parameter-shift")
def run_circuit(theta1, theta2):
    """
    Input a numpy array feature (which encodes a single normalized angle encoded spot price)
    """
    Loader(theta1, theta2, wires=[0, 1])
    return qml.expval(qml.PauliX(0))

def cost_func(x, theta1, theta2):
    output = run_circuit(theta1, theta2)
    return output + x

theta1 = np.array(0.1, requires_grad=True)
theta2 = np.array(0.2, requires_grad=True)
x = np.array(0.1, requires_grad = False)

opt = qml.AdamOptimizer(0.1)

for _ in range(10):
    grad, _ = opt.compute_grad(cost_func, (x,theta1, theta2), {})
    updates = opt.apply_grad(grad, (x,theta1, theta2)) 
    theta = updates[0]
    theta2 = updates[1]
    print(grad)

Nice! There are examples of using one argument for each variable or one argument for multiple variables (an array). For instance,

  • qml.Rot(phi, theta, omega, wires=0)
  • qml.AngleEmbedding(features, wires=wires), where features is an array

There are other examples, but I think for your case, making it so that Loader takes two single arguments, theta1 and theta2, makes sense to me!

Hi Isaac,

Thank you for the help!

Best,
Nikhil

Awesome! Glad I could help!