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!

1 Like

Hi Isaac,

Thank you for the help!

Best,
Nikhil

1 Like

Awesome! Glad I could help!