github PennyLaneAI/pennylane v0.19.0
Release 0.19.0

latest releases: v0.38.1, v0.38.0-rc0, v0.38.0...
2 years ago

New features since last release

Differentiable Hartree-Fock solver

  • A differentiable Hartree-Fock (HF) solver has been added. It can be used to construct molecular Hamiltonians that can be differentiated with respect to nuclear coordinates and basis-set parameters. (#1610)

    The HF solver computes the integrals over basis functions, constructs the relevant matrices, and performs self-consistent-field iterations to obtain a set of optimized molecular orbital coefficients. These coefficients and the computed integrals over basis functions are used to construct the one- and two-body electron integrals in the molecular orbital basis which can be used to generate a differentiable second-quantized Hamiltonian in the fermionic and qubit basis.

    The following code shows the construction of the Hamiltonian for the hydrogen molecule where the geometry of the molecule is differentiable.

    symbols = ["H", "H"]
    geometry = np.array([[0.0000000000, 0.0000000000, -0.6943528941],
                         [0.0000000000, 0.0000000000,  0.6943528941]], requires_grad=True)
    
    mol = qml.hf.Molecule(symbols, geometry)
    args_mol = [geometry]
    
    hamiltonian = qml.hf.generate_hamiltonian(mol)(*args_mol)
    >>> hamiltonian.coeffs
    tensor([-0.09041082+0.j,  0.17220382+0.j,  0.17220382+0.j,
             0.16893367+0.j,  0.04523101+0.j, -0.04523101+0.j,
            -0.04523101+0.j,  0.04523101+0.j, -0.22581352+0.j,
             0.12092003+0.j, -0.22581352+0.j,  0.16615103+0.j,
             0.16615103+0.j,  0.12092003+0.j,  0.17464937+0.j], requires_grad=True)

    The generated Hamiltonian can be used in a circuit where the atomic coordinates and circuit parameters are optimized simultaneously.

    symbols = ["H", "H"]
    geometry = np.array([[0.0000000000, 0.0000000000, 0.0],
                         [0.0000000000, 0.0000000000, 2.0]], requires_grad=True)
    
    mol = qml.hf.Molecule(symbols, geometry)
    
    dev = qml.device("default.qubit", wires=4)
    params = [np.array([0.0], requires_grad=True)]
    
    def generate_circuit(mol):
        @qml.qnode(dev)
        def circuit(*args):
            qml.BasisState(np.array([1, 1, 0, 0]), wires=[0, 1, 2, 3])
            qml.DoubleExcitation(*args[0][0], wires=[0, 1, 2, 3])
            return qml.expval(qml.hf.generate_hamiltonian(mol)(*args[1:]))
        return circuit
    
    for n in range(25):
    
        mol = qml.hf.Molecule(symbols, geometry)
        args = [params, geometry] # initial values of the differentiable parameters
    
        g_params = qml.grad(generate_circuit(mol), argnum = 0)(*args)
        params = params - 0.5 * g_params[0]
    
        forces = qml.grad(generate_circuit(mol), argnum = 1)(*args)
        geometry = geometry - 0.5 * forces
    
        print(f'Step: {n}, Energy: {generate_circuit(mol)(*args)}, Maximum Force: {forces.max()}')

    In addition, the new Hartree-Fock solver can further be used to optimize the basis set parameters. For details, please refer to the differentiable Hartree-Fock solver documentation.

Integration with Mitiq

  • Error mitigation using the zero-noise extrapolation method is now available through the transforms.mitigate_with_zne transform. This transform can integrate with the Mitiq package for unitary folding and extrapolation functionality. (#1813)

    Consider the following noisy device:

    noise_strength = 0.05
    dev = qml.device("default.mixed", wires=2)
    dev = qml.transforms.insert(qml.AmplitudeDamping, noise_strength)(dev)

    We can mitigate the effects of this noise for circuits run on this device by using the added
    transform:

    from mitiq.zne.scaling import fold_global
    from mitiq.zne.inference import RichardsonFactory
    
    n_wires = 2
    n_layers = 2
    
    shapes = qml.SimplifiedTwoDesign.shape(n_wires, n_layers)
    np.random.seed(0)
    w1, w2 = [np.random.random(s) for s in shapes]
    
    @qml.transforms.mitigate_with_zne([1, 2, 3], fold_global, RichardsonFactory.extrapolate)
    @qml.beta.qnode(dev)
    def circuit(w1, w2):
        qml.SimplifiedTwoDesign(w1, w2, wires=range(2))
        return qml.expval(qml.PauliZ(0))

    Now, when we execute circuit, errors will be automatically mitigated:

    >>> circuit(w1, w2)
    0.19113067083636542

Powerful new transforms

  • The unitary matrix corresponding to a quantum circuit can now be generated using the new get_unitary_matrix() transform. (#1609) (#1786)

    This transform is fully differentiable across all supported PennyLane autodiff frameworks.

    def circuit(theta):
        qml.RX(theta, wires=1)
        qml.PauliZ(wires=0)
        qml.CNOT(wires=[0, 1])
    >>> theta = torch.tensor(0.3, requires_grad=True)
    >>> matrix = qml.transforms.get_unitary_matrix(circuit)(theta)
    >>> print(matrix)
    tensor([[ 0.9888+0.0000j,  0.0000+0.0000j,  0.0000-0.1494j,  0.0000+0.0000j],
          [ 0.0000+0.0000j,  0.0000+0.1494j,  0.0000+0.0000j, -0.9888+0.0000j],
          [ 0.0000-0.1494j,  0.0000+0.0000j,  0.9888+0.0000j,  0.0000+0.0000j],
          [ 0.0000+0.0000j, -0.9888+0.0000j,  0.0000+0.0000j,  0.0000+0.1494j]],
         grad_fn=<MmBackward>)
    >>> loss = torch.real(torch.trace(matrix))
    >>> loss.backward()
    >>> theta.grad
    tensor(-0.1494)
  • Arbitrary two-qubit unitaries can now be decomposed into elementary gates. This functionality has been incorporated into the qml.transforms.unitary_to_rot transform, and is available separately as qml.transforms.two_qubit_decomposition. (#1552)

    As an example, consider the following randomly-generated matrix and circuit that uses it:

    U = np.array([
        [-0.03053706-0.03662692j,  0.01313778+0.38162226j, 0.4101526 -0.81893687j, -0.03864617+0.10743148j],
        [-0.17171136-0.24851809j,  0.06046239+0.1929145j, -0.04813084-0.01748555j, -0.29544883-0.88202604j],
        [ 0.39634931-0.78959795j, -0.25521689-0.17045233j, -0.1391033 -0.09670952j, -0.25043606+0.18393466j],
        [ 0.29599198-0.19573188j,  0.55605806+0.64025769j, 0.06140516+0.35499559j,  0.02674726+0.1563311j ]
    ])
    
    dev = qml.device('default.qubit', wires=2)
    
    @qml.qnode(dev)
    @qml.transforms.unitary_to_rot
    def circuit(x, y):
        qml.QubitUnitary(U, wires=[0, 1])
        return qml.expval(qml.PauliZ(wires=0))

    If we run the circuit, we can see the new decomposition:

    >>> circuit(0.3, 0.4)
    tensor(-0.81295986, requires_grad=True)
    >>> print(qml.draw(circuit)(0.3, 0.4))
    0: ──Rot(2.78, 0.242, -2.28)──╭X──RZ(0.176)───╭C─────────────╭X──Rot(-3.87, 0.321, -2.09)──┤ ⟨Z⟩
    1: ──Rot(4.64, 2.69, -1.56)───╰C──RY(-0.883)──╰X──RY(-1.47)──╰C──Rot(1.68, 0.337, 0.587)───┤
  • A new transform, @qml.batch_params, has been added, that makes QNodes handle a batch dimension in trainable parameters. (#1710) (#1761)

    This transform will create multiple circuits, one per batch dimension. As a result, it is both simulator and hardware compatible.

    @qml.batch_params
    @qml.beta.qnode(dev)
    def circuit(x, weights):
        qml.RX(x, wires=0)
        qml.RY(0.2, wires=1)
        qml.templates.StronglyEntanglingLayers(weights, wires=[0, 1, 2])
        return qml.expval(qml.Hadamard(0))

    The qml.batch_params decorator allows us to pass arguments x and weights that have a batch dimension. For example,

    >>> batch_size = 3
    >>> x = np.linspace(0.1, 0.5, batch_size)
    >>> weights = np.random.random((batch_size, 10, 3, 3))

    If we evaluate the QNode with these inputs, we will get an output of shape (batch_size,):

    >>> circuit(x, weights)
    tensor([0.08569816, 0.12619101, 0.21122004], requires_grad=True)
  • The insert transform has now been added, providing a way to insert single-qubit operations into a quantum circuit. The transform can apply to quantum functions, tapes, and devices. (#1795)

    The following QNode can be transformed to add noise to the circuit:

    dev = qml.device("default.mixed", wires=2)
    
    @qml.qnode(dev)
    @qml.transforms.insert(qml.AmplitudeDamping, 0.2, position="end")
    def f(w, x, y, z):
        qml.RX(w, wires=0)
        qml.RY(x, wires=1)
        qml.CNOT(wires=[0, 1])
        qml.RY(y, wires=0)
        qml.RX(z, wires=1)
        return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))

    Executions of this circuit will differ from the noise-free value:

    >>> f(0.9, 0.4, 0.5, 0.6)
    tensor(0.754847, requires_grad=True)
    >>> print(qml.draw(f)(0.9, 0.4, 0.5, 0.6))
     0: ──RX(0.9)──╭C──RY(0.5)──AmplitudeDamping(0.2)──╭┤ ⟨Z ⊗ Z⟩
     1: ──RY(0.4)──╰X──RX(0.6)──AmplitudeDamping(0.2)──╰┤ ⟨Z ⊗ Z⟩
  • Common tape expansion functions are now available in qml.transforms, alongside a new create_expand_fn function for easily creating expansion functions from stopping criteria. (#1734) (#1760)

    create_expand_fn takes the default depth to which the expansion function should expand a tape, a stopping criterion, an optional device, and a docstring to be set for the created function. The stopping criterion must take a queuable object and return a boolean.

    For example, to create an expansion function that decomposes all trainable, multi-parameter operations:

    >>> stop_at = ~(qml.operation.has_multipar & qml.operation.is_trainable)
    >>> expand_fn = qml.transforms.create_expand_fn(depth=5, stop_at=stop_at)

    The created expansion function can be used within a custom transform. Devices can also be provided, producing expansion functions that decompose tapes to support the native gate set of the device.

Batch execution of circuits

  • A new, experimental QNode has been added, that adds support for batch execution of circuits, custom quantum gradient support, and arbitrary order derivatives. This QNode is available via qml.beta.QNode, and @qml.beta.qnode. (#1642) (#1646) (#1651) (#1804)

    It differs from the standard QNode in several ways:

    • Custom gradient transforms can be specified as the differentiation method:

      @qml.gradients.gradient_transform
      def my_gradient_transform(tape):
          ...
          return tapes, processing_fn
      
      @qml.beta.qnode(dev, diff_method=my_gradient_transform)
      def circuit():
    • Arbitrary :math:n-th order derivatives are supported on hardware using gradient transforms such as the parameter-shift rule. To specify that an :math:n-th order derivative of a QNode will be computed, the max_diff argument should be set. By default, this is set to 1 (first-order derivatives only).

    • Internally, if multiple circuits are generated for execution simultaneously, they will be packaged into a single job for execution on the device. This can lead to significant performance improvement when executing the QNode on remote quantum hardware.

    • When decomposing the circuit, the default decomposition strategy will prioritize decompositions that result in the smallest number of parametrized operations required to satisfy the differentiation method. Additional decompositions required to satisfy the native gate set of the quantum device will be performed later, by the device at execution time. While this may lead to a slight increase in classical processing, it significantly reduces the number of circuit evaluations needed to compute gradients of complex unitaries.

    In an upcoming release, this QNode will replace the existing one. If you come across any bugs while using this QNode, please let us know via a bug report on our GitHub bug tracker.

    Currently, this beta QNode does not support the following features:

    • Non-mutability via the mutable keyword argument
    • The reversible QNode differentiation method
    • The ability to specify a dtype when using PyTorch and TensorFlow.

    It is also not tested with the qml.qnn module.

New operations and templates

  • Added a new operation OrbitalRotation, which implements the spin-adapted spatial orbital rotation gate. (#1665)

    An example circuit that uses OrbitalRotation operation is:

    dev = qml.device('default.qubit', wires=4)
    
    @qml.qnode(dev)
    def circuit(phi):
        qml.BasisState(np.array([1, 1, 0, 0]), wires=[0, 1, 2, 3])
        qml.OrbitalRotation(phi, wires=[0, 1, 2, 3])
        return qml.state()

    If we run this circuit, we will get the following output

    >>> circuit(0.1)
    array([ 0.        +0.j,  0.        +0.j,  0.        +0.j,
            0.00249792+0.j,  0.        +0.j,  0.        +0.j,
            -0.04991671+0.j,  0.        +0.j,  0.        +0.j,
            -0.04991671+0.j,  0.        +0.j,  0.        +0.j,
            0.99750208+0.j,  0.        +0.j,  0.        +0.j,
            0.        +0.j])
  • Added a new template GateFabric, which implements a local, expressive, quantum-number-preserving ansatz proposed by Anselmetti et al. in arXiv:2104.05692. (#1687)

    An example of a circuit using GateFabric template is:

    coordinates = np.array([0.0, 0.0, -0.6614, 0.0, 0.0, 0.6614])
    H, qubits = qml.qchem.molecular_hamiltonian(["H", "H"], coordinates)
    ref_state = qml.qchem.hf_state(electrons=2, orbitals=qubits)
    
    dev = qml.device('default.qubit', wires=qubits)
    
    @qml.qnode(dev)
    def ansatz(weights):
        qml.templates.GateFabric(weights, wires=[0,1,2,3],
                                    init_state=ref_state, include_pi=True)
        return qml.expval(H)

    For more details, see the GateFabric documentation.

  • Added a new template kUpCCGSD, which implements a unitary coupled cluster ansatz with generalized singles and pair doubles excitation operators, proposed by Joonho Lee et al. in arXiv:1810.02327. (#1743)

    An example of a circuit using kUpCCGSD template is:

    coordinates = np.array([0.0, 0.0, -0.6614, 0.0, 0.0, 0.6614])
    H, qubits = qml.qchem.molecular_hamiltonian(["H", "H"], coordinates)
    ref_state = qml.qchem.hf_state(electrons=2, orbitals=qubits)
    
    dev = qml.device('default.qubit', wires=qubits)
    
    @qml.qnode(dev)
    def ansatz(weights):
        qml.templates.kUpCCGSD(weights, wires=[0,1,2,3], k=0, delta_sz=0,
                                    init_state=ref_state)
        return qml.expval(H)

Improved utilities for quantum compilation and characterization

  • The new qml.fourier.qnode_spectrum function extends the former qml.fourier.spectrum function and takes classical processing of QNode arguments into account. The frequencies are computed per (requested) QNode argument instead of per gate id. The gate ids are ignored. (#1681) (#1720)

    Consider the following example, which uses non-trainable inputs x, y and z as well as trainable parameters w as arguments to the QNode.

    import pennylane as qml
    import numpy as np
    
    n_qubits = 3
    dev = qml.device("default.qubit", wires=n_qubits)
    
    @qml.qnode(dev)
    def circuit(x, y, z, w):
        for i in range(n_qubits):
            qml.RX(0.5*x[i], wires=i)
            qml.Rot(w[0,i,0], w[0,i,1], w[0,i,2], wires=i)
            qml.RY(2.3*y[i], wires=i)
            qml.Rot(w[1,i,0], w[1,i,1], w[1,i,2], wires=i)
            qml.RX(z, wires=i)
        return qml.expval(qml.PauliZ(wires=0))
    
    x = np.array([1., 2., 3.])
    y = np.array([0.1, 0.3, 0.5])
    z = -1.8
    w = np.random.random((2, n_qubits, 3))

    This circuit looks as follows:

    >>> print(qml.draw(circuit)(x, y, z, w))
    0: ──RX(0.5)──Rot(0.598, 0.949, 0.346)───RY(0.23)──Rot(0.693, 0.0738, 0.246)──RX(-1.8)──┤ ⟨Z⟩
    1: ──RX(1)────Rot(0.0711, 0.701, 0.445)──RY(0.69)──Rot(0.32, 0.0482, 0.437)───RX(-1.8)──┤
    2: ──RX(1.5)──Rot(0.401, 0.0795, 0.731)──RY(1.15)──Rot(0.756, 0.38, 0.38)─────RX(-1.8)──┤

    Applying the qml.fourier.qnode_spectrum function to the circuit for the non-trainable parameters, we obtain:

    >>> spec = qml.fourier.qnode_spectrum(circuit, encoding_args={"x", "y", "z"})(x, y, z, w)
    >>> for inp, freqs in spec.items():
    ...     print(f"{inp}: {freqs}")
    "x": {(0,): [-0.5, 0.0, 0.5], (1,): [-0.5, 0.0, 0.5], (2,): [-0.5, 0.0, 0.5]}
    "y": {(0,): [-2.3, 0.0, 2.3], (1,): [-2.3, 0.0, 2.3], (2,): [-2.3, 0.0, 2.3]}
    "z": {(): [-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0]}

    We can see that all three parameters in the QNode arguments x and y contribute the spectrum of a Pauli rotation [-1.0, 0.0, 1.0], rescaled with the prefactor of the respective parameter in the circuit. The three RX rotations using the parameter z accumulate, yielding a more complex frequency spectrum.

    For details on how to control for which parameters the spectrum is computed, a comparison to qml.fourier.circuit_spectrum, and other usage details, please see the fourier.qnode_spectrum docstring.

  • Two new methods were added to the Device API, allowing PennyLane devices increased control over circuit decompositions. (#1651)

    • Device.expand_fn(tape) -> tape: expands a tape such that it is supported by the device. By default, performs the standard device-specific gate set decomposition done in the default QNode. Devices may overwrite this method in order to define their own decomposition logic.

      Note that the numerical result after applying this method should remain unchanged; PennyLane will assume that the expanded tape returns exactly the same value as the original tape when executed.

    • Device.batch_transform(tape) -> (tapes, processing_fn): preprocesses the tape in the case where the device needs to generate multiple circuits to execute from the input circuit. The requirement of a post-processing function makes this distinct to the expand_fn method above.

      By default, this method applies the transform

      .. math:: \left\langle \sum_i c_i h_i\right\rangle → \sum_i c_i \left\langle h_i \right\rangle

      if expval(H) is present on devices that do not natively support Hamiltonians with non-commuting terms.

  • A new class has been added to store operator attributes, such as self_inverses, and composable_rotation, as a list of operation names. (#1763)

    A number of such attributes, for the purpose of compilation transforms, can be found in ops/qubit/attributes.py, but the class can also be used to create your own. For example, we can create a new Attribute, pauli_ops, like so:

    >>> from pennylane.ops.qubit.attributes import Attribute
    >>> pauli_ops = Attribute(["PauliX", "PauliY", "PauliZ"])

    We can check either a string or an Operation for inclusion in this set:

    >>> qml.PauliX(0) in pauli_ops
    True
    >>> "Hadamard" in pauli_ops
    False

    We can also dynamically add operators to the sets at runtime. This is useful for adding custom operations to the attributes such as composable_rotations and self_inverses that are used in compilation transforms. For example, suppose you have created a new Operation, MyGate, which you know to be its own inverse. Adding it to the set, like so

    >>> from pennylane.ops.qubit.attributes import self_inverses
    >>> self_inverses.add("MyGate")

    will enable the gate to be considered by the cancel_inverses compilation transform if two such gates are adjacent in a circuit.

Improvements

  • The qml.metric_tensor transform has been improved with regards to both function and performance. (#1638) (#1721)

    • If the underlying device supports batch execution of circuits, the quantum circuits required to compute the metric tensor elements will be automatically submitted as a batched job. This can lead to significant performance improvements for devices with a non-trivial job submission overhead.

    • Previously, the transform would only return the metric tensor with respect to gate arguments, and ignore any classical processing inside the QNode, even very trivial classical processing such as parameter permutation. The metric tensor now takes into account classical processing, and returns the metric tensor with respect to QNode arguments, not simply gate arguments:

      >>> @qml.qnode(dev)
      ... def circuit(x):
      ...     qml.Hadamard(wires=1)
      ...     qml.RX(x[0], wires=0)
      ...     qml.CNOT(wires=[0, 1])
      ...     qml.RY(x[1] ** 2, wires=1)
      ...     qml.RY(x[1], wires=0)
      ...     return qml.expval(qml.PauliZ(0))
      >>> x = np.array([0.1, 0.2], requires_grad=True)
      >>> qml.metric_tensor(circuit)(x)
      array([[0.25      , 0.        ],
             [0.        , 0.28750832]])

      To revert to the previous behaviour of returning the metric tensor with respect to gate arguments, qml.metric_tensor(qnode, hybrid=False) can be passed.

      >>> qml.metric_tensor(circuit, hybrid=False)(x)
      array([[0.25      , 0.        , 0.        ],
             [0.        , 0.25      , 0.        ],
             [0.        , 0.        , 0.24750832]])
    • The metric tensor transform now works with a larger set of operations. In particular, all operations that have a single variational parameter and define a generator are now
      supported. In addition to a reduction in decomposition overhead, the change also results in fewer circuit evaluations.

  • The expansion rule in the qml.metric_tensor transform has been changed. (#1721)

    If hybrid=False, the changed expansion rule might lead to a changed output.

  • The ApproxTimeEvolution template can now be used with Hamiltonians that have trainable coefficients. (#1789)

    The resulting QNodes can be differentiated with respect to both the time parameter and the Hamiltonian coefficients.

    dev = qml.device('default.qubit', wires=2)
    obs = [qml.PauliX(0) @ qml.PauliY(1), qml.PauliY(0) @ qml.PauliX(1)]
    
    @qml.qnode(dev)
    def circuit(coeffs, t):
        H = qml.Hamiltonian(coeffs, obs)
        qml.templates.ApproxTimeEvolution(H, t, 2)
        return qml.expval(qml.PauliZ(0))
    >>> t = np.array(0.54, requires_grad=True)
    >>> coeffs = np.array([-0.6, 2.0], requires_grad=True)
    >>> qml.grad(circuit)(coeffs, t)
    (array([-1.07813375, -1.07813375]), array(-2.79516158))

    All differentiation methods, including backpropagation and the parameter-shift rule, are supported.

  • Quantum function transforms and batch transforms can now be applied to devices. Once applied to a device, any quantum function executed on the modified device will be transformed prior to execution. (#1809) (#1810)

    dev = qml.device("default.mixed", wires=1)
    dev = qml.transforms.merge_rotations()(dev)
    
    @qml.beta.qnode(dev)
    def f(w, x, y, z):
        qml.RX(w, wires=0)
        qml.RX(x, wires=0)
        qml.RX(y, wires=0)
        qml.RX(z, wires=0)
        return qml.expval(qml.PauliZ(0))
    >>> print(f(0.9, 0.4, 0.5, 0.6))
     -0.7373937155412453
    >>> print(qml.draw(f, expansion_strategy="device")(0.9, 0.4, 0.5, 0.6))
     0: ──RX(2.4)──┤ ⟨Z⟩
  • It is now possible to draw QNodes that have been transformed by a 'batch transform'; that is, a transform that maps a single QNode into multiple circuits under the hood. Examples of batch transforms include @qml.metric_tensor and @qml.gradients. (#1762)

    For example, consider the parameter-shift rule, which generates two circuits per parameter; one circuit that has the parameter shifted forward, and another that has the parameter shifted backwards:

    dev = qml.device("default.qubit", wires=2)
    
    @qml.gradients.param_shift
    @qml.beta.qnode(dev)
    def circuit(x):
        qml.RX(x, wires=0)
        qml.CNOT(wires=[0, 1])
        return qml.expval(qml.PauliZ(wires=0))
    >>> print(qml.draw(circuit)(0.6))
     0: ──RX(2.17)──╭C──┤ ⟨Z⟩
     1: ────────────╰X──┤
    
     0: ──RX(-0.971)──╭C──┤ ⟨Z⟩
     1: ──────────────╰X──┤
  • Support for differentiable execution of batches of circuits has been extended to the JAX interface for scalar functions, via the beta pennylane.interfaces.batch module. (#1634) (#1685)

    For example using the execute function from the pennylane.interfaces.batch module:

    from pennylane.interfaces.batch import execute
    
    def cost_fn(x):
        with qml.tape.JacobianTape() as tape1:
            qml.RX(x[0], wires=[0])
            qml.RY(x[1], wires=[1])
            qml.CNOT(wires=[0, 1])
            qml.var(qml.PauliZ(0) @ qml.PauliX(1))
    
        with qml.tape.JacobianTape() as tape2:
            qml.RX(x[0], wires=0)
            qml.RY(x[0], wires=1)
            qml.CNOT(wires=[0, 1])
            qml.probs(wires=1)
    
        result = execute(
          [tape1, tape2], dev,
          gradient_fn=qml.gradients.param_shift,
          interface="autograd"
        )
        return (result[0] + result[1][0, 0])[0]
    
    res = jax.grad(cost_fn)(params)
  • All qubit operations have been re-written to use the qml.math framework for internal classical processing and the generation of their matrix representations. As a result these representations are now fully differentiable, and the framework-specific device classes no longer need to maintain framework-specific versions of these matrices. (#1749) (#1802)

  • The use of expval(H), where H is a cost Hamiltonian generated by the qaoa module, has been sped up. This was achieved by making PennyLane decompose a circuit with an expval(H) measurement into subcircuits if the Hamiltonian.grouping_indices attribute is set, and setting this attribute in the relevant qaoa module functions. (#1718)

  • Operations can now have gradient recipes that depend on the state of the operation. (#1674)

    For example, this allows for gradient recipes that are parameter dependent:

    class RX(qml.RX):
    
        @property
        def grad_recipe(self):
            # The gradient is given by [f(2x) - f(0)] / (2 sin(x)), by subsituting
            # shift = x into the two term parameter-shift rule.
            x = self.data[0]
            c = 0.5 / np.sin(x)
            return ([[c, 0.0, 2 * x], [-c, 0.0, 0.0]],)
  • Shots can now be passed as a runtime argument to transforms that execute circuits in batches, similarly to QNodes. (#1707)

    An example of such a transform are the gradient transforms in the qml.gradients module. As a result, we can now call gradient transforms (such as qml.gradients.param_shift) and set the number of shots at runtime.

    >>> dev = qml.device("default.qubit", wires=1, shots=1000)
    >>> @qml.beta.qnode(dev)
    ... def circuit(x):
    ...     qml.RX(x, wires=0)
    ...     return qml.expval(qml.PauliZ(0))
    >>> grad_fn = qml.gradients.param_shift(circuit)
    >>> param = np.array(0.564, requires_grad=True)
    >>> grad_fn(param, shots=[(1, 10)]).T
    array([[-1., -1., -1., -1., -1.,  0., -1.,  0., -1.,  0.]])
    >>> param2 = np.array(0.1233, requires_grad=True)
    >>> grad_fn(param2, shots=None)
    array([[-0.12298782]])
  • Templates are now top level imported and can be used directly e.g. qml.QFT(wires=0). (#1779)

  • qml.probs now accepts an attribute op that allows to rotate the computational basis and get the probabilities in the rotated basis. (#1692)

  • Refactored the expand_fn functionality in the Device class to avoid any edge cases leading to failures with plugins. (#1838)

  • Updated the qml.QNGOptimizer.step_and_cost method to avoid the use of deprecated functionality. (#1834)

  • Added a custom torch.to_numpy implementation to pennylane/math/single_dispatch.py to ensure compabilitity with PyTorch 1.10. (#1824) (#1825)

  • The default for an Operation's control_wires attribute is now an empty Wires object instead of the attribute raising a NonImplementedError. (#1821)

  • qml.circuit_drawer.MPLDrawer will now automatically rotate and resize text to fit inside the rectangle created by the box_gate method. (#1764)

  • Operators now have a label method to determine how they are drawn. This will eventually override the RepresentationResolver class. (#1678)

  • The operation label method now supports string variables. (#1815)

  • A new utility class qml.BooleanFn is introduced. It wraps a function that takes a single argument and returns a Boolean. (#1734)

    After wrapping, qml.BooleanFn can be called like the wrapped function, and multiple instances can be manipulated and combined with the bitwise operators &, | and ~.

  • There is a new utility function qml.math.is_independent that checks whether a callable is independent of its arguments. (#1700)

    This function is experimental and might behave differently than expected.

    Note that the test relies on both numerical and analytical checks, except when using the PyTorch interface which only performs a numerical check. It is known that there are edge cases on which this test will yield wrong results, in particular non-smooth functions may be problematic. For details, please refer to the is_indpendent docstring.

  • The qml.beta.QNode now supports the qml.qnn module. (#1748)

  • @qml.beta.QNode now supports the qml.specs transform. (#1739)

  • qml.circuit_drawer.drawable_layers and qml.circuit_drawer.drawable_grid process a list of operations to layer positions for drawing. (#1639)

  • qml.transforms.batch_transform now accepts expand_fns that take additional arguments and keyword arguments. In fact, expand_fn and transform_fn now must have the same signature. (#1721)

  • The qml.batch_transform decorator is now ignored during Sphinx builds, allowing the correct signature to display in the built documentation. (#1733)

  • The tests for qubit operations are split into multiple files. (#1661)

  • The transform for the Jacobian of the classical preprocessing within a QNode, qml.transforms.classical_jacobian, now takes a keyword argument argnum to specify the QNode argument indices with respect to which the Jacobian is computed. (#1645)

    An example for the usage of argnum is

    @qml.qnode(dev)
    def circuit(x, y, z):
        qml.RX(qml.math.sin(x), wires=0)
        qml.CNOT(wires=[0, 1])
        qml.RY(y ** 2, wires=1)
        qml.RZ(1 / z, wires=1)
        return qml.expval(qml.PauliZ(0))
    
    jac_fn = qml.transforms.classical_jacobian(circuit, argnum=[1, 2])

    The Jacobian can then be computed at specified parameters.

    >>> x, y, z = np.array([0.1, -2.5, 0.71])
    >>> jac_fn(x, y, z)
    (array([-0., -5., -0.]), array([-0.        , -0.        , -1.98373339]))

    The returned arrays are the derivatives of the three parametrized gates in the circuit with respect to y and z respectively.

    There also are explicit tests for classical_jacobian now, which previously was tested implicitly via its use in the metric_tensor transform.

    For more usage details, please see the classical Jacobian docstring.

  • A new utility function qml.math.is_abstract(tensor) has been added. This function returns True if the tensor is abstract; that is, it has no value or shape. This can occur if within a function that has been just-in-time compiled. (#1845)

  • qml.circuit_drawer.CircuitDrawer can accept a string for the charset keyword, instead of a CharSet object. (#1640)

  • qml.math.sort will now return only the sorted torch tensor and not the corresponding indices, making sort consistent across interfaces. (#1691)

  • Specific QNode execution options are now re-used by batch transforms to execute transformed QNodes. (#1708)

  • To standardize across all optimizers, qml.optimize.AdamOptimizer now also uses accumulation (in form of collections.namedtuple) to keep track of running quantities. Before it used three variables fm, sm and t. (#1757)

Breaking changes

  • The operator attributes has_unitary_generator, is_composable_rotation, is_self_inverse, is_symmetric_over_all_wires, and is_symmetric_over_control_wires have been removed as attributes from the base class. They have been replaced by the sets that store the names of operations with similar properties in ops/qubit/attributes.py. (#1763)

  • The qml.inv function has been removed, qml.adjoint should be used instead. (#1778)

  • The input signature of an expand_fn used in a batch_transform now must have the same signature as the provided transform_fn, and vice versa. (#1721)

  • The default.qubit.torch device automatically determines if computations should be run on a CPU or a GPU and doesn't take a torch_device argument anymore. (#1705)

  • The utility function qml.math.requires_grad now returns True when using Autograd if and only if the requires_grad=True attribute is set on the NumPy array. Previously, this function would return True for all NumPy arrays and Python floats, unless requires_grad=False was explicitly set. (#1638)

  • The operation qml.Interferometer has been renamed qml.InterferometerUnitary in order to distinguish it from the template qml.templates.Interferometer. (#1714)

  • The qml.transforms.invisible decorator has been replaced with qml.tape.stop_recording, which may act as a context manager as well as a decorator to ensure that contained logic is non-recordable or non-queueable within a QNode or quantum tape context. (#1754)

  • Templates SingleExcitationUnitary and DoubleExcitationUnitary have been renamed to FermionicSingleExcitation and FermionicDoubleExcitation, respectively. (#1822)

Deprecations

  • Allowing cost functions to be differentiated using qml.grad or qml.jacobian without explicitly marking parameters as trainable is being deprecated, and will be removed in an upcoming release. Please specify the requires_grad attribute for every argument, or specify argnum when using qml.grad or qml.jacobian. (#1773)

    The following raises a warning in v0.19.0 and will raise an error in an upcoming release:

     import pennylane as qml
    
    dev = qml.device('default.qubit', wires=1)
    
    @qml.qnode(dev)   def test(x):       qml.RY(x, wires=[0])
        return qml.expval(qml.PauliZ(0))
    
    par = 0.3
    qml.grad(test)(par)

    Preferred approaches include specifying the requires_grad attribute:

    import pennylane as qml
    from pennylane import numpy as np
    
    dev = qml.device('default.qubit', wires=1)
    
    @qml.qnode(dev)
    def test(x):
        qml.RY(x, wires=[0])
        return qml.expval(qml.PauliZ(0))
    
    par = np.array(0.3, requires_grad=True)
    qml.grad(test)(par)

    Or specifying the argnum argument when using qml.grad or qml.jacobian:

    import pennylane as qml
    
    dev = qml.device('default.qubit', wires=1)
    
    @qml.qnode(dev)
    def test(x):
        qml.RY(x, wires=[0])
        return qml.expval(qml.PauliZ(0))
    
    par = 0.3
    qml.grad(test, argnum=0)(par)
  • The default.tensor device from the beta folder is no longer maintained and has been deprecated. It will be removed in future releases. (#1851)

  • The qml.metric_tensor and qml.QNGOptimizer keyword argument diag_approx is deprecated. Approximations can be controlled with the more fine-grained approx keyword argument, with approx="block-diag" (the default) reproducing the old behaviour. (#1721) (#1834)

  • The template decorator is now deprecated with a warning message and will be removed in release v0.20.0. It has been removed from different PennyLane functions. (#1794) (#1808)

  • The qml.fourier.spectrum function has been renamed to qml.fourier.circuit_spectrum, in order to clearly separate the new qnode_spectrum function from this one. qml.fourier.spectrum is now an alias for circuit_spectrum but is flagged for deprecation and will be removed soon. (#1681)

  • The init module, which contains functions to generate random parameter tensors for templates, is flagged for deprecation and will be removed in the next release cycle. Instead, the templates' shape method can be used to get the desired shape of the tensor, which can then be generated manually. (#1689)

    To generate the parameter tensors, the np.random.normal and np.random.uniform functions can be used (just like in the init module). Considering the default arguments of these functions as of NumPy v1.21, some non-default options were used by the init module:

    • All functions generating normally distributed parameters used np.random.normal by passing scale=0.1;

    • Most functions generating uniformly distributed parameters (except for certain CVQNN initializers) used np.random.uniform by passing high=2*math.pi;

    • The cvqnn_layers_r_uniform, cvqnn_layers_a_uniform, cvqnn_layers_kappa_uniform functions used np.random.uniform by passing high=0.1.

  • The QNode.draw method has been deprecated, and will be removed in an upcoming release. Please use the qml.draw transform instead. (#1746)

  • The QNode.metric_tensor method has been deprecated, and will be removed in an upcoming release. Please use the qml.metric_tensor transform instead. (#1638)

  • The pad parameter of the qml.AmplitudeEmbedding template has been removed. It has instead been renamed to the pad_with parameter. (#1805)

Bug fixes

  • Fixes a bug where qml.math.dot failed to work with @tf.function autograph mode. (#1842)

  • Fixes a bug where in rare instances the parameters of a tape are returned unsorted by Tape.get_parameters. (#1836)

  • Fixes a bug with the arrow width in the measure of qml.circuit_drawer.MPLDrawer. (#1823)

  • The helper functions qml.math.block_diag and qml.math.scatter_element_add now are entirely differentiable when using Autograd. Previously only indexed entries of the block diagonal could be differentiated, while the derivative w.r.t to the second argument of qml.math.scatter_element_add dispatched to NumPy instead of Autograd. (#1816) (#1818)

  • Fixes a bug such that the original shot vector information of a device is preserved, so that outside the context manager the device remains unchanged. (#1792)

  • Modifies qml.math.take to be compatible with a breaking change released in JAX 0.2.24 and ensure that PennyLane supports this JAX version. (#1769)

  • Fixes a bug where the GPU cannot be used with qml.qnn.TorchLayer. (#1705)

  • Fix a bug where the devices cache the same result for different observables return types. (#1719)

  • Fixed a bug of the default circuit drawer where having more measurements compared to the number of measurements on any wire raised a KeyError. (#1702)

  • Fix a bug where it was not possible to use jax.jit on a QNode when using QubitStateVector. (#1683)

  • The device suite tests can now execute successfully if no shots configuration variable is given. (#1641)

  • Fixes a bug where the qml.gradients.param_shift transform would raise an error while attempting to compute the variance of a QNode with ragged output. (#1646)

  • Fixes a bug in default.mixed, to ensure that returned probabilities are always non-negative. (#1680)

  • Fixes a bug where gradient transforms would fail to apply to QNodes containing classical processing. (#1699)

  • Fixes a bug where the parameter-shift method was not correctly using the fallback gradient function when all circuit parameters required the fallback. (#1782)

Documentation

  • Adds a link to https://pennylane.ai/qml/demonstrations.html in the navbar. (#1624)

  • Corrects the docstring of ExpvalCost by adding wires to the signature of the ansatz argument. (#1715)

  • Updated docstring examples using the qchem.molecular_hamiltonian function. (#1724)

  • Updates the 'Gradients and training' quickstart guide to provide information on gradient transforms. (#1751)

  • All instances of qnode.draw() have been updated to instead use the transform qml.draw(qnode). (#1750)

  • Add the jax interface in QNode Documentation. (#1755)

  • Reorganized all the templates related to quantum chemistry under a common header Quantum Chemistry templates. (#1822)

Contributors

This release contains contributions from (in alphabetical order):

Catalina Albornoz, Juan Miguel Arrazola, Utkarsh Azad, Akash Narayanan B, Sam Banning, Thomas Bromley, Jack Ceroni, Alain Delgado, Olivia Di Matteo, Andrew Gardhouse, Anthony Hayes, Theodor Isacsson, David Ittah, Josh Izaac, Soran Jahangiri, Nathan Killoran, Christina Lee, Guillermo Alonso-Linaje, Romain Moyard, Lee James O'Riordan, Carrie-Anne Rubidge, Maria Schuld, Rishabh Singh, Jay Soni, Ingrid Strandberg, Antal Száva, Teresa Tamayo-Mendoza, Rodrigo Vargas, Cody Wang, David Wierichs, Moritz Willmann.

Don't miss a new pennylane release

NewReleases is sending notifications on new releases.