PennyLaneAI / comments

0 stars 0 forks source link

blog/2021/08/how-to-write-quantum-function-transforms-in-pennylane/ #4

Open utterances-bot opened 2 years ago

utterances-bot commented 2 years ago

How to write quantum function transforms in PennyLane

How to write quantum function transforms in PennyLane | The latest release of PennyLane, version 0.17, contains a number of new features based on...

https://pennylane.ai/blog/2021/08/how-to-write-quantum-function-transforms-in-pennylane/

santoshkumarradha commented 2 years ago

Could someone from pennylane team outline how transforms work with qml.templates ? Trying to transform a function with template in it turns out to be non-trivial as one needs to somehow extract the operations from them.

josh146 commented 2 years ago

Hi @santoshkumarradha, this is a very good question!

When PennyLane applies quantum function transforms, these apply directly to the provided quantum function, even if that quantum function contains templates. This is intended to provide full flexibility ---this way, you can write a transform that modifies template parameters directly (for example, UCCSD(weight) -> UCCSD(sin(weights))).

However, there are often cases where we would want to transform the underlying operations that constitute a template. In this case, we need to first expand the quantum tape:

tape = tape.expand(stop_at, depth)

Here, stop_at is a function that specifies whether the expansion should continue given a particular operation being present. depth determines how deeply to perform the expansion before ending.

Here is a quick example. I have taken the overrotate_x transform above, and added in a step to first expand out the tape, stopping once the parametrized operations are simply Pauli rotations:

stop_condition = lambda op: (
    isinstance(op, qml.measure.MeasurementProcess)
    or op.num_params == 0
    or op.name in ("RX", "RY", "RZ", "PhaseShift")
)

@qml.qfunc_transform
def overrotate_rx(tape, overrot_angle=0):
    tape = qml.transforms.invisible(tape.expand)(stop_at=stop_condition, depth=5)

    for op in tape.operations + tape.measurements:
        if op.name == "RX":
            qml.RX(op.parameters[0] + qml.math.sqrt(overrot_angle), wires=op.wires)
        else:
            qml.apply(op)

We can now apply this directly to a more complicated quantum function, that includes a template:

>>> dev = qml.device("default.qubit", wires=3)
>>> @qml.qnode(dev)
... @overrotate_rx(overrot_angle=0.05)
... def circuit(phi):
...     qml.templates.SingleExcitationUnitary(phi, wires=[0, 1, 2])
...     return qml.expval(qml.PauliZ(0) @ qml.PauliX(2))
>>> print(qml.draw(circuit)(0.5))
 0: ──RX(-1.35)──╭C────────────────────╭C──RX(1.79)───H──╭C─────────────────────╭C─────────H──╭┤ ⟨Z ⊗ X⟩
 1: ─────────────╰X──╭C────────────╭C──╰X────────────────╰X──╭C─────────────╭C──╰X────────────│┤
 2: ──H──────────────╰X──RZ(0.25)──╰X───H──RX(-1.35)─────────╰X──RZ(-0.25)──╰X───RX(1.79)─────╰┤ ⟨Z ⊗ X⟩
josh146 commented 2 years ago

This is really good feedback however -- I will investigate and see if we can ensure quantum function transforms intergrate better with templates :slightly_smiling_face:

ghost commented 2 years ago

Thank you @josh146 for the quick reply. Addition of transforms are extremely powerful to us as we have essentially been having an internal hack to deal with this part till now. Often times our use case is to transform circuits with many templates to various gates supported by plugins as well as introduce custom noise / error correction parameters/gates.

jakobhuhn commented 1 year ago

Hello PennyLane team,

Thank you for the great tutorial. Is it possible to introduce new parameters in the qfunction transforms that are themselves trainable again? And how would I specify in qml.grad to train these new parameters as well?

Thank you for your help!

Best regards, Jakob

josh146 commented 1 year ago

Hi @jakobhuhn! Do you have a code example you could share?

jakobhuhn commented 1 year ago

Hi @josh146,

So as a simple example I have a circuit containing a two-qubit CZ gate and two trainable parameters:

import pennylane as qml
from pennylane import numpy as np

def simple_circuit(theta):
    qml.RX(theta[0], wires=0)
    qml.RY(theta[1], wires=1)
    qml.CZ(wires=[0, 1])
    return qml.expval(qml.grouping.string_to_pauli_word("ZZ"))

Now I would like to swap the two-qubit gate with two pramaterized gates (as was done in https://arxiv.org/abs/2203.13739). For this I created the following quantum function transform.

@qml.qfunc_transform
def convertCZ(tape, new_params):
    count = 0
    for op in tape:
        if op.name =='CZ':
            wires = op.wires
            qml.PhaseShift(np.pi*new_params[count], wires=wires[0])
            count += 1
            qml.PhaseShift(np.pi*new_params[count], wires=wires[1])
        else:
            qml.apply(op)

Now the Circuit has 4 parameters and I would like to train all of them so I pass trainable parameters to the quantum function transform

theta = np.array([0.5, 0.6], requires_grad=True)
new_params = np.array([0.7, 0.8], requires_grad=True)
dev = qml.device("default.qubit", wires=2)
new_qfunc = convertCZ(new_params)(simple_circuit)
qnode = qml.QNode(new_qfunc, dev)

Running qml.gradient only gives the gradient for the two original parameters of the simple circuit:

>>>gradient = qml.grad(qnode)
>>>gradient(theta)
[-0.39568697 -0.49552039]

However qml.specs says that there are indeed four trainable parameters in the qnode:

>>>qml.specs(qnode)(theta)
{'gate_sizes': defaultdict(<class 'int'>, {1: 4}),
 'gate_types': defaultdict(<class 'int'>, {'RX': 1, 'RY': 1, 'PhaseShift': 2}),
 'num_operations': 4, 
'num_observables': 1,
 'num_diagonalizing_gates': 0,
 'num_used_wires': 2,
 'depth': 2, 
'num_trainable_params': 4, 
'num_device_wires': 2, 
'device_name': 'default.qubit.autograd', 
'expansion_strategy': 'gradient', 
'gradient_options': {}, 
'interface': 'autograd', 
'diff_method': 'best', 
'gradient_fn': 'backprop'}

How can I train all parameters, even the ones introduced by the quantum function transform.

Thank you a lot for your help!

Best regards, Jakob

josh146 commented 1 year ago

Hey @jakobhuhn! The reason is that when you call qml.grad, you are applying it to a function that only takes theta as its arguments.

Instead, what you want to do is write a cost function that takes both theta and new_params as input -- that way, qml.grad will be able to 'see' and differentiate both.

For example:

def cost(theta, new_params):
    new_qfunc = convertCZ(new_params)(simple_circuit)
    qnode = qml.QNode(new_qfunc, dev)
    return qnode(theta)

gradient = qml.grad(cost)
print(gradient(theta, new_params))
jakobhuhn commented 1 year ago

Thank you!