Advanced Usage

In the previous three introductory tutorials (qubit rotation, Gaussian transformation, and plugins & hybrid computation) we explored the basic concepts of PennyLane, including qubit- and CV-model quantum computations, gradient-based optimization, and the construction of hybrid classical-quantum computations. In this tutorial, we will highlight some of the more advanced features of Pennylane.

Multiple measurements

In all the previous examples, we considered quantum functions with only single expectation values. In fact, PennyLane supports the return of multiple measurements, up to one per wire.

As usual, we begin by importing PennyLane and the PennyLane-provided version of NumPy, and set up a 2-wire qubit device for computations:

import pennylane as qml
from pennylane import numpy as np

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

We will start with a simple example circuit, which generates a two-qubit entangled state, then evaluates the expectation value of the Pauli Z operator on each wire.

@qml.qnode(dev)
def circuit(param):
    qml.RX(param, wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))

The degree of entanglement of the qubits is determined by the value of param. For a value of \(\frac{\pi}{2}\), they are maximally entangled. In this case, the reduced states on each subsystem are completely mixed, and local expectation values — like those we are measuring — will average to zero.

print(circuit(np.pi / 2))

Out:

[2.22044605e-16 2.22044605e-16]

Notice that the output of the circuit is a NumPy array with shape=(2,), i.e., a two-dimensional vector. These two dimensions match the number of expectation values returned in our quantum function circuit.

Note

It is important to emphasize that the expectation values in circuit are both local, i.e., this circuit is evaluating \(\left\langle \sigma_z\right\rangle_0\) and \(\left\langle \sigma_z\right\rangle_1\), not \(\left\langle \sigma_z\otimes \sigma_z\right\rangle_{01}\) (where the subscript denotes which wires the observable is located on).

In order to measure a tensor-product observable like \(\langle\sigma_z \otimes \sigma_z \rangle _{01}\), the matrix multiplication operator @ can be used:

@qml.qnode(dev)
def circuit(param):
    qml.RX(param, wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))

print(circuit(np.pi / 2))

Out:

1.0

Notice how this expectation value differs from the local versions above.

We may even mix different return types, for example expectation values and variances:

@qml.qnode(dev)
def circuit(param):
    qml.RX(param, wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0)), qml.var(qml.PauliZ(1))

Keyword arguments

While automatic differentiation is a handy feature, sometimes we want certain parts of our computational pipeline (e.g., the inputs \(x\) to a parameterized quantum function \(f(x;\bf{\theta})\) or the training data for a machine learning model) to not be differentiated.

PennyLane uses the pattern that all positional arguments to quantum functions are available to be differentiated, while keyword arguments are never differentiated. Thus, when using the gradient-descent-based Optimizers included in PennyLane, all numerical parameters appearing in non-keyword arguments will be updated, while all numerical values included as keyword arguments will not be updated.

Note

When constructing the circuit, keyword arguments are defined by providing a default value in the function signature. If you would prefer that the keyword argument value be passed every time the quantum circuit function is called, the default value can be set to None.

For example, let’s create a quantum node that accepts two arguments; a differentiable circuit parameter param, and a fixed circuit parameter fixed:

@qml.qnode(dev)
def circuit(param, fixed=None):
    qml.RX(fixed, wires=0)
    qml.RX(param, wires=1)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))

Calling the circuit, we can feed values to the keyword argument fixed:

print(circuit(0.1, fixed=-0.2))

print(circuit(0.1, fixed=1.2))

Out:

[0.98006658 0.97517033]
[0.36235775 0.36054748]

Since keyword arguments do not get considered when computing gradients, the Jacobian will still be a 2-dimensional vector.

j = qml.jacobian(circuit, argnum=0)
print(j(2.5, fixed=3.2))

Out:

[0.         0.59745161]

Once defined, keyword arguments must always be passed as keyword arguments. PennyLane does not support passing keyword argument values as positional arguments.

For example, the following circuit evaluation will correctly update the value of the fixed parameter:

print(circuit(0.1, fixed=0.4))

Out:

[0.92106099 0.91645953]

However, attempting to pass the fixed parameter as a positional argument will not work, and PennyLane will attempt to use the default value (None) instead:

>>> circuit(0.1, 0.4)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-949e31911afa> in <module>()
----> 1 circuit(0.1, 0.4)
~/pennylane/variable.py in val(self)
    134
    135         # The variable is a placeholder for a keyword argument
--> 136         value = self.kwarg_values[self.name][self.idx] * self.mult
    137         return value
TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'

Total running time of the script: ( 0 minutes 0.485 seconds)

Gallery generated by Sphinx-Gallery