Skip to content

Release 0.15.0

Compare
Choose a tag to compare
@josh146 josh146 released this 20 Apr 07:06
· 2672 commits to master since this release
6252d3e

New features since last release

Better and more flexible shot control

  • Adds a new optimizer qml.ShotAdaptiveOptimizer, a gradient-descent optimizer where the shot rate is adaptively calculated using the variances of the parameter-shift gradient. (#1139)

    By keeping a running average of the parameter-shift gradient and the variance of the parameter-shift gradient, this optimizer frugally distributes a shot budget across the partial derivatives of each parameter.

    In addition, if computing the expectation value of a Hamiltonian, weighted random sampling can be used to further distribute the shot budget across the local terms from which the Hamiltonian is constructed.

    This optimizer is based on both the iCANS1 and Rosalin shot-adaptive optimizers.

    Once constructed, the cost function can be passed directly to the optimizer's step method. The attribute opt.total_shots_used can be used to track the number of shots per iteration.

    >>> coeffs = [2, 4, -1, 5, 2]
    >>> obs = [
    ...   qml.PauliX(1),
    ...   qml.PauliZ(1),
    ...   qml.PauliX(0) @ qml.PauliX(1),
    ...   qml.PauliY(0) @ qml.PauliY(1),
    ...   qml.PauliZ(0) @ qml.PauliZ(1)
    ... ]
    >>> H = qml.Hamiltonian(coeffs, obs)
    >>> dev = qml.device("default.qubit", wires=2, shots=100)
    >>> cost = qml.ExpvalCost(qml.templates.StronglyEntanglingLayers, H, dev)
    >>> params = qml.init.strong_ent_layers_uniform(n_layers=2, n_wires=2)
    >>> opt = qml.ShotAdaptiveOptimizer(min_shots=10)
    >>> for i in range(5):
    ...    params = opt.step(cost, params)
    ...    print(f"Step {i}: cost = {cost(params):.2f}, shots_used = {opt.total_shots_used}")
    Step 0: cost = -5.68, shots_used = 240
    Step 1: cost = -2.98, shots_used = 336
    Step 2: cost = -4.97, shots_used = 624
    Step 3: cost = -5.53, shots_used = 1054
    Step 4: cost = -6.50, shots_used = 1798
  • Batches of shots can now be specified as a list, allowing measurement statistics to be course-grained with a single QNode evaluation. (#1103)

    >>> shots_list = [5, 10, 1000]
    >>> dev = qml.device("default.qubit", wires=2, shots=shots_list)

    When QNodes are executed on this device, a single execution of 1015 shots will be submitted. However, three sets of measurement statistics will be returned; using the first 5 shots, second set of 10 shots, and final 1000 shots, separately.

    For example, executing a circuit with two outputs will lead to a result of shape (3, 2):

    >>> @qml.qnode(dev)
    ... def circuit(x):
    ...     qml.RX(x, wires=0)
    ...     qml.CNOT(wires=[0, 1])
    ...     return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)), qml.expval(qml.PauliZ(0))
    >>> circuit(0.5)
    [[0.33333333 1.        ]
     [0.2        1.        ]
     [0.012      0.868     ]]

    This output remains fully differentiable.

  • The number of shots can now be specified on a per-call basis when evaluating a QNode. (#1075).

    For this, the qnode should be called with an additional shots keyword argument:

    >>> dev = qml.device('default.qubit', wires=1, shots=10) # default is 10
    >>> @qml.qnode(dev)
    ... def circuit(a):
    ...     qml.RX(a, wires=0)
    ...     return qml.sample(qml.PauliZ(wires=0))
    >>> circuit(0.8)
    [ 1  1  1 -1 -1  1  1  1  1  1]
    >>> circuit(0.8, shots=3)
    [ 1  1  1]
    >>> circuit(0.8)
    [ 1  1  1 -1 -1  1  1  1  1  1]

New differentiable quantum transforms

A new module is available, qml.transforms, which contains differentiable quantum transforms. These are functions that act on QNodes, quantum functions, devices, and tapes, transforming them while remaining fully differentiable.

  • A new adjoint transform has been added. (#1111) (#1135)

    This new method allows users to apply the adjoint of an arbitrary sequence of operations.

    def subroutine(wire):
        qml.RX(0.123, wires=wire)
        qml.RY(0.456, wires=wire)
    
    dev = qml.device('default.qubit', wires=1)
    @qml.qnode(dev)
    def circuit():
        subroutine(0)
        qml.adjoint(subroutine)(0)
        return qml.expval(qml.PauliZ(0))

    This creates the following circuit:

    >>> print(qml.draw(circuit)())
    0: --RX(0.123)--RY(0.456)--RY(-0.456)--RX(-0.123)--| <Z>

    Directly applying to a gate also works as expected.

    qml.adjoint(qml.RX)(0.123, wires=0) # applies RX(-0.123)
  • A new transform qml.ctrl is now available that adds control wires to subroutines. (#1157)

    def my_ansatz(params):
       qml.RX(params[0], wires=0)
       qml.RZ(params[1], wires=1)
    
    # Create a new operation that applies `my_ansatz`
    # controlled by the "2" wire.
    my_ansatz2 = qml.ctrl(my_ansatz, control=2)
    
    @qml.qnode(dev)
    def circuit(params):
        my_ansatz2(params)
        return qml.state()

    This is equivalent to:

    @qml.qnode(...)
    def circuit(params):
        qml.CRX(params[0], wires=[2, 0])
        qml.CRZ(params[1], wires=[2, 1])
        return qml.state()
  • The qml.transforms.classical_jacobian transform has been added. (#1186)

    This transform returns a function to extract the Jacobian matrix of the classical part of a QNode, allowing the classical dependence between the QNode arguments and the quantum gate arguments to be extracted.

    For example, given the following QNode:

    >>> @qml.qnode(dev)
    ... def circuit(weights):
    ...     qml.RX(weights[0], wires=0)
    ...     qml.RY(weights[0], wires=1)
    ...     qml.RZ(weights[2] ** 2, wires=1)
    ...     return qml.expval(qml.PauliZ(0))

    We can use this transform to extract the relationship :math:f: \mathbb{R}^n \rightarrow\mathbb{R}^m between the input QNode arguments :math:w and the gate arguments :math:g, for a given value of the QNode arguments:

    >>> cjac_fn = qml.transforms.classical_jacobian(circuit)
    >>> weights = np.array([1., 1., 1.], requires_grad=True)
    >>> cjac = cjac_fn(weights)
    >>> print(cjac)
    [[1. 0. 0.]
     [1. 0. 0.]
     [0. 0. 2.]]

    The returned Jacobian has rows corresponding to gate arguments, and columns corresponding to QNode arguments; that is, :math:J_{ij} = \frac{\partial}{\partial g_i} f(w_j).

More operations and templates

  • Added the SingleExcitation two-qubit operation, which is useful for quantum chemistry applications. (#1121)

    It can be used to perform an SO(2) rotation in the subspace spanned by the states :math:|01\rangle and :math:|10\rangle. For example, the following circuit performs the transformation :math:|10\rangle \rightarrow \cos(\phi/2)|10\rangle - \sin(\phi/2)|01\rangle:

    dev = qml.device('default.qubit', wires=2)
    
    @qml.qnode(dev)
    def circuit(phi):
        qml.PauliX(wires=0)
        qml.SingleExcitation(phi, wires=[0, 1])

    The SingleExcitation operation supports analytic gradients on hardware using only four expectation value calculations, following results from Kottmann et al.

  • Added the DoubleExcitation four-qubit operation, which is useful for quantum chemistry applications. (#1123)

    It can be used to perform an SO(2) rotation in the subspace spanned by the states :math:|1100\rangle and :math:|0011\rangle. For example, the following circuit performs the transformation :math:|1100\rangle\rightarrow \cos(\phi/2)|1100\rangle - \sin(\phi/2)|0011\rangle:

    dev = qml.device('default.qubit', wires=2)
    
    @qml.qnode(dev)
    def circuit(phi):
        qml.PauliX(wires=0)
        qml.PauliX(wires=1)
        qml.DoubleExcitation(phi, wires=[0, 1, 2, 3])

    The DoubleExcitation operation supports analytic gradients on hardware using only four expectation value calculations, following results from Kottmann et al..

  • Added the QuantumMonteCarlo template for performing quantum Monte Carlo estimation of an expectation value on simulator. (#1130)

    The following example shows how the expectation value of sine squared over a standard normal distribution can be approximated:

    from scipy.stats import norm
    
    m = 5
    M = 2 ** m
    n = 10
    N = 2 ** n
    target_wires = range(m + 1)
    estimation_wires = range(m + 1, n + m + 1)
    
    xmax = np.pi  # bound to region [-pi, pi]
    xs = np.linspace(-xmax, xmax, M)
    
    probs = np.array([norm().pdf(x) for x in xs])
    probs /= np.sum(probs)
    
    func = lambda i: np.sin(xs[i]) ** 2
    
    dev = qml.device("default.qubit", wires=(n + m + 1))
    
    @qml.qnode(dev)
    def circuit():
        qml.templates.QuantumMonteCarlo(
            probs,
            func,
            target_wires=target_wires,
            estimation_wires=estimation_wires,
        )
        return qml.probs(estimation_wires)
    
    phase_estimated = np.argmax(circuit()[:int(N / 2)]) / N
    expectation_estimated = (1 - np.cos(np.pi * phase_estimated)) / 2
  • Added the QuantumPhaseEstimation template for performing quantum phase estimation for an input unitary matrix. (#1095)

    Consider the matrix corresponding to a rotation from an RX gate:

    >>> phase = 5
    >>> target_wires = [0]
    >>> unitary = qml.RX(phase, wires=0).matrix

    The phase parameter can be estimated using QuantumPhaseEstimation. For example, using five phase-estimation qubits:

    n_estimation_wires = 5
    estimation_wires = range(1, n_estimation_wires + 1)
    
    dev = qml.device("default.qubit", wires=n_estimation_wires + 1)
    
    @qml.qnode(dev)
    def circuit():
        # Start in the |+> eigenstate of the unitary
        qml.Hadamard(wires=target_wires)
    
        QuantumPhaseEstimation(
            unitary,
            target_wires=target_wires,
            estimation_wires=estimation_wires,
        )
    
        return qml.probs(estimation_wires)
    
    phase_estimated = np.argmax(circuit()) / 2 ** n_estimation_wires
    
    # Need to rescale phase due to convention of RX gate
    phase_estimated = 4 * np.pi * (1 - phase)
  • Added the ControlledPhaseShift gate as well as the QFT operation for applying quantum Fourier transforms. (#1064)

    @qml.qnode(dev)
    def circuit_qft(basis_state):
        qml.BasisState(basis_state, wires=range(3))
        qml.QFT(wires=range(3))
        return qml.state()
  • Added the ControlledQubitUnitary operation. This enables implementation of multi-qubit gates with a variable number of control qubits. It is also possible to specify a different state for the control qubits using the control_values argument (also known as a mixed-polarity multi-controlled operation). (#1069) (#1104)

    For example, we can create a multi-controlled T gate using:

    T = qml.T._matrix()
    qml.ControlledQubitUnitary(T, control_wires=[0, 1, 3], wires=2, control_values="110")

    Here, the T gate will be applied to wire 2 if control wires 0 and 1 are in state 1, and control wire 3 is in state 0. If no value is passed to control_values, the gate will be applied if all control wires are in the 1 state.

  • Added MultiControlledX for multi-controlled NOT gates.
    This is a special case of ControlledQubitUnitary that applies a
    Pauli X gate conditioned on the state of an arbitrary number of
    control qubits.
    (#1104)

Support for higher-order derivatives on hardware

  • Computing second derivatives and Hessians of QNodes is now supported with the parameter-shift differentiation method, on all machine learning interfaces. (#1130) (#1129) (#1110)

    Hessians are computed using the parameter-shift rule, and can be evaluated on both hardware and simulator devices.

    dev = qml.device('default.qubit', wires=1)
    
    @qml.qnode(dev, diff_method="parameter-shift")
    def circuit(p):
        qml.RY(p[0], wires=0)
        qml.RX(p[1], wires=0)
        return qml.expval(qml.PauliZ(0))
    
    x = np.array([1.0, 2.0], requires_grad=True)
    >>> hessian_fn = qml.jacobian(qml.grad(circuit))
    >>> hessian_fn(x)
    [[0.2248451 0.7651474]
     [0.7651474 0.2248451]]
  • Added the function finite_diff() to compute finite-difference approximations to the gradient and the second-order derivatives of arbitrary callable functions. (#1090)

    This is useful to compute the derivative of parametrized pennylane.Hamiltonian observables with respect to their parameters.

    For example, in quantum chemistry simulations it can be used to evaluate the derivatives of the electronic Hamiltonian with respect to the nuclear coordinates:

    >>> def H(x):
    ...    return qml.qchem.molecular_hamiltonian(['H', 'H'], x)[0]
    >>> x = np.array([0., 0., -0.66140414, 0., 0., 0.66140414])
    >>> grad_fn = qml.finite_diff(H, N=1)
    >>> grad = grad_fn(x)
    >>> deriv2_fn = qml.finite_diff(H, N=2, idx=[0, 1])
    >>> deriv2_fn(x)
  • The JAX interface now supports all devices, including hardware devices, via the parameter-shift differentiation method. (#1076)

    For example, using the JAX interface with Cirq:

    dev = qml.device('cirq.simulator', wires=1)
    @qml.qnode(dev, interface="jax", diff_method="parameter-shift")
    def circuit(x):
        qml.RX(x[1], wires=0)
        qml.Rot(x[0], x[1], x[2], wires=0)
        return qml.expval(qml.PauliZ(0))
    weights = jnp.array([0.2, 0.5, 0.1])
    print(circuit(weights))

    Currently, when used with the parameter-shift differentiation method, only a single returned expectation value or variance is supported. Multiple expectations/variances, as well as probability and state returns, are not currently allowed.

Improvements

  • The MottonenStatePreparation template has improved performance on states with only real amplitudes by reducing the number of redundant CNOT gates at the end of a circuit.

    dev = qml.device("default.qubit", wires=2)
    
    inputstate = [np.sqrt(0.2), np.sqrt(0.3), np.sqrt(0.4), np.sqrt(0.1)]
    
    @qml.qnode(dev)
    def circuit():
        mottonen.MottonenStatePreparation(inputstate,wires=[0, 1])
        return qml.expval(qml.PauliZ(0))

    Previously returned:

    >>> print(qml.draw(circuit)())
    0: ──RY(1.57)──╭C─────────────╭C──╭C──╭C──┤ ⟨Z⟩ 
    1: ──RY(1.35)──╰X──RY(0.422)──╰X──╰X──╰X──┤   

    In this release, it now returns:

    >>> print(qml.draw(circuit)())
    0: ──RY(1.57)──╭C─────────────╭C──┤ ⟨Z⟩ 
    1: ──RY(1.35)──╰X──RY(0.422)──╰X──┤   
  • The templates are now classes inheriting from Operation, and define the ansatz in their expand() method. This change does not affect the user interface. (#1138) (#1156) (#1163) (#1192)

    For convenience, some templates have a new method that returns the expected shape of the trainable parameter tensor, which can be used to create random tensors.

    shape = qml.templates.BasicEntanglerLayers.shape(n_layers=2, n_wires=4)
    weights = np.random.random(shape)
    qml.templates.BasicEntanglerLayers(weights, wires=range(4))
  • QubitUnitary now validates to ensure the input matrix is two dimensional. (#1128)

  • Most layers in Pytorch or Keras accept arbitrary dimension inputs, where each dimension barring the last (in the case where the actual weight function of the layer operates on one-dimensional vectors) is broadcast over. This is now also supported by KerasLayer and TorchLayer. (#1062).

    Example use:

    dev = qml.device("default.qubit", wires=4)
    x = tf.ones((5, 4, 4))
    
    @qml.qnode(dev)
    def layer(weights, inputs):
        qml.templates.AngleEmbedding(inputs, wires=range(4))
        qml.templates.StronglyEntanglingLayers(weights, wires=range(4))
        return [qml.expval(qml.PauliZ(i)) for i in range(4)]
    
    qlayer = qml.qnn.KerasLayer(layer, {"weights": (4, 4, 3)}, output_dim=4)
    out = qlayer(x)

    The output tensor has the following shape:

    >>> out.shape
    (5, 4, 4)
  • If only one argument to the function qml.grad has the requires_grad attribute set to True, then the returned gradient will be a NumPy array, rather than a tuple of length 1. (#1067) (#1081)

  • An improvement has been made to how QubitDevice generates and post-processess samples, allowing QNode measurement statistics to work on devices with more than 32 qubits. (#1088)

  • Due to the addition of density_matrix() as a return type from a QNode, tuples are now supported by the output_dim parameter in qnn.KerasLayer. (#1070)

  • Two new utility methods are provided for working with quantum tapes. (#1175)

    • qml.tape.get_active_tape() gets the currently recording tape.

    • tape.stop_recording() is a context manager that temporarily stops the currently recording tape from recording additional tapes or quantum operations.

    For example:

    >>> with qml.tape.QuantumTape():
    ...     qml.RX(0, wires=0)
    ...     current_tape = qml.tape.get_active_tape()
    ...     with current_tape.stop_recording():
    ...         qml.RY(1.0, wires=1)
    ...     qml.RZ(2, wires=1)
    >>> current_tape.operations
    [RX(0, wires=[0]), RZ(2, wires=[1])]
  • When printing qml.Hamiltonian objects, the terms are sorted by number of wires followed by coefficients. (#981)

  • Adds qml.math.conj to the PennyLane math module. (#1143)

    This new method will do elementwise conjugation to the given tensor-like object, correctly dispatching to the required tensor-manipulation framework to preserve differentiability.

    >>> a = np.array([1.0 + 2.0j])
    >>> qml.math.conj(a)
    array([1.0 - 2.0j])
  • The four-term parameter-shift rule, as used by the controlled rotation operations, has been updated to use coefficients that minimize the variance as per https://arxiv.org/abs/2104.05695. (#1206)

  • A new transform qml.transforms.invisible has been added, to make it easier to transform QNodes. (#1175)

Breaking changes

  • Devices do not have an analytic argument or attribute anymore. Instead, shots is the source of truth for whether a simulator estimates return values from a finite number of shots, or whether it returns analytic results (shots=None). (#1079) (#1196)

    dev_analytic = qml.device('default.qubit', wires=1, shots=None)
    dev_finite_shots = qml.device('default.qubit', wires=1, shots=1000)
    
    def circuit():
        qml.Hadamard(wires=0)
        return qml.expval(qml.PauliZ(wires=0))
    
    circuit_analytic = qml.QNode(circuit, dev_analytic)
    circuit_finite_shots = qml.QNode(circuit, dev_finite_shots)

    Devices with shots=None return deterministic, exact results:

    >>> circuit_analytic()
    0.0
    >>> circuit_analytic()
    0.0

    Devices with shots > 0 return stochastic results estimated from
    samples in each run:

    >>> circuit_finite_shots()
    -0.062
    >>> circuit_finite_shots()
    0.034

    The qml.sample() measurement can only be used on devices on which the number of shots is set explicitly.

  • If creating a QNode from a quantum function with an argument named shots, a DeprecationWarning is raised, warning the user that this is a reserved argument to change the number of shots on a per-call basis. (#1075)

  • For devices inheriting from QubitDevice, the methods expval, var, sample accept two new keyword arguments --- shot_range and bin_size. (#1103)

    These new arguments allow for the statistics to be performed on only a subset of device samples. This finer level of control is accessible from the main UI by instantiating a device with a batch of shots.

    For example, consider the following device:

    >>> dev = qml.device("my_device", shots=[5, (10, 3), 100])

    This device will execute QNodes using 135 shots, however measurement statistics will be course grained across these 135 shots:

    • All measurement statistics will first be computed using the first 5 shots --- that is, shots_range=[0, 5], bin_size=5.

    • Next, the tuple (10, 3) indicates 10 shots, repeated 3 times. This will use shot_range=[5, 35], performing the expectation value in bins of size 10 (bin_size=10).

    • Finally, we repeat the measurement statistics for the final 100 shots, shot_range=[35, 135], bin_size=100.

  • The old PennyLane core has been removed, including the following modules: (#1100)

    • pennylane.variables
    • pennylane.qnodes

    As part of this change, the location of the new core within the Python
    module has been moved:

    • Moves pennylane.tape.interfacespennylane.interfaces
    • Merges pennylane.CircuitGraph and pennylane.TapeCircuitGraphpennylane.CircuitGraph
    • Merges pennylane.OperationRecorder and pennylane.TapeOperationRecorder
    • pennylane.tape.operation_recorder
    • Merges pennylane.measure and pennylane.tape.measurepennylane.measure
    • Merges pennylane.operation and pennylane.tape.operationpennylane.operation
    • Merges pennylane._queuing and pennylane.tape.queuingpennylane.queuing

    This has no affect on import location.

    In addition,

    • All tape-mode functions have been removed (qml.enable_tape(), qml.tape_mode_active()),
    • All tape fixtures have been deleted,
    • Tests specifically for non-tape mode have been deleted.
  • The device test suite no longer accepts the analytic keyword. (#1216)

Bug fixes

  • Fixes a bug where using the circuit drawer with a ControlledQubitUnitary operation raised an error. (#1174)

  • Fixes a bug and a test where the QuantumTape.is_sampled attribute was not being updated. (#1126)

  • Fixes a bug where BasisEmbedding would not accept inputs whose bits are all ones or all zeros. (#1114)

  • The ExpvalCost class raises an error if instantiated with non-expectation measurement statistics. (#1106)

  • Fixes a bug where decompositions would reset the differentiation method of a QNode. (#1117)

  • Fixes a bug where the second-order CV parameter-shift rule would error if attempting to compute the gradient of a QNode with more than one second-order observable. (#1197)

  • Fixes a bug where repeated Torch interface applications after expansion caused an error. (#1223)

  • Sampling works correctly with batches of shots specified as a list. (#1232)

Documentation

  • Updated the diagram used in the Architectural overview page of the Development guide such that it doesn't mention Variables. (#1235)

  • Typos addressed in templates documentation. (#1094)

  • Upgraded the documentation to use Sphinx 3.5.3 and the new m2r2 package. (#1186)

  • Added flaky as dependency for running tests in the documentation. (#1113)

Contributors

This release contains contributions from (in alphabetical order):

Shahnawaz Ahmed, Juan Miguel Arrazola, Thomas Bromley, Olivia Di Matteo, Alain Delgado Gran, Kyle Godbey, Diego Guala, Theodor Isacsson, Josh Izaac, Soran Jahangiri, Nathan Killoran, Christina Lee, Daniel Polatajko, Chase Roberts, Sankalp Sanand, Pritish Sehzpaul, Maria Schuld, Antal Száva, David Wierichs.