Skip to content

Releases: PennyLaneAI/pennylane

Release 0.25.1

18 Aug 10:01
49cb810
Compare
Choose a tag to compare

Bug fixes

  • Fixed Torch device discrepencies for certain parametrized operations by updating qml.math.array and qml.math.eye to preserve the Torch device used. (#2967)

Contributors

This release contains contributions from (in alphabetical order):

Romain Moyard, Rashid N H M, Lee James O'Riordan, Antal Száva.

Release 0.25.0

15 Aug 18:34
1c8b8b8
Compare
Choose a tag to compare

New features since last release

Estimate computational resource requirements 🧠

  • Functionality for estimating molecular simulation computations has been added with qml.resource. (#2646) (#2653) (#2665) (#2694) (#2720) (#2723) (#2746) (#2796) (#2797) (#2874) (#2944) (#2644)

    The new resource module allows you to estimate the number of non-Clifford gates and logical qubits needed to implement quantum phase estimation algorithms for simulating materials and molecules. This includes support for quantum algorithms using first and second quantization with specific bases:

    • First quantization using a plane-wave basis via the FirstQuantization class:

      >>> n = 100000        # number of plane waves
      >>> eta = 156         # number of electrons
      >>> omega = 1145.166  # unit cell volume in atomic units
      >>> algo = FirstQuantization(n, eta, omega)
      >>> print(algo.gates, algo.qubits)
      1.10e+13, 4416
    • Second quantization with a double-factorized Hamiltonian via the DoubleFactorization class:

      symbols = ["O", "H", "H"]
      geometry = np.array(
          [
              [0.00000000, 0.00000000, 0.28377432],
              [0.00000000, 1.45278171, -1.00662237],
              [0.00000000, -1.45278171, -1.00662237],
          ],
          requires_grad=False,
      )
      
      mol = qml.qchem.Molecule(symbols, geometry, basis_name="sto-3g")
      core, one, two = qml.qchem.electron_integrals(mol)()
      
      algo = DoubleFactorization(one, two)
      >>> print(algo.gates, algo.qubits)
      103969925, 290

    The methods of the FirstQuantization and the DoubleFactorization classes, such as qubit_cost (number of logical qubits) and gate_cost (number of non-Clifford gates), can be also accessed as static methods:

    >>> qml.resource.FirstQuantization.qubit_cost(100000, 156, 169.69608, 0.01) 
    4377 
    >>> qml.resource.FirstQuantization.gate_cost(100000, 156, 169.69608, 0.01) 
    3676557345574

Differentiable error mitigation ⚙️

  • Differentiable zero-noise-extrapolation (ZNE) error mitigation is now available. (#2757)

    Elevate any variational quantum algorithm to a mitigated algorithm with improved results on noisy hardware while maintaining differentiability throughout.

    In order to do so, use the qml.transforms.mitigate_with_zne transform on your QNode and provide the PennyLane proprietary qml.transforms.fold_global folding function and qml.transforms.poly_extrapolate extrapolation function. Here is an example for a noisy simulation device where we mitigate a QNode and are still able to compute the gradient:

    # Describe noise
    noise_gate = qml.DepolarizingChannel
    noise_strength = 0.1
    
    # Load devices
    dev_ideal = qml.device("default.mixed", wires=1)
    dev_noisy = qml.transforms.insert(noise_gate, noise_strength)(dev_ideal)
    
    scale_factors = [1, 2, 3]
    @mitigate_with_zne(
      scale_factors,
      qml.transforms.fold_global,
      qml.transforms.poly_extrapolate,
      extrapolate_kwargs={'order': 2}
    )
    @qml.qnode(dev_noisy)
    def qnode_mitigated(theta):
        qml.RY(theta, wires=0)
        return qml.expval(qml.PauliX(0))
    >>> theta = np.array(0.5, requires_grad=True)
    >>> qml.grad(qnode_mitigated)(theta)
    0.5712737447327619

More native support for parameter broadcasting 📡

  • default.qubit now natively supports parameter broadcasting, providing increased performance when executing the same circuit at various parameter positions compared to manually looping over parameters, or directly using the qml.transforms.broadcast_expand transform. (#2627)

    dev = qml.device("default.qubit", wires=1)
    
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x, wires=0)
        return qml.expval(qml.PauliZ(0))
    >>> circuit(np.array([0.1, 0.3, 0.2]))
    tensor([0.99500417, 0.95533649, 0.98006658], requires_grad=True) 

    Currently, not all templates have been updated to support broadcasting.

  • Parameter-shift gradients now allow for parameter broadcasting internally, which can result in a significant speedup when computing gradients of circuits with many parameters. (#2749)

    The gradient transform qml.gradients.param_shift now accepts the keyword argument broadcast. If set to True, broadcasting is used to compute the derivative:

    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev)
    def circuit(x, y):
        qml.RX(x, wires=0)
        qml.RY(y, wires=1)
        return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
    >>> x = np.array([np.pi/3, np.pi/2], requires_grad=True)
    >>> y = np.array([np.pi/6, np.pi/5], requires_grad=True)
    >>> qml.gradients.param_shift(circuit, broadcast=True)(x, y)
    (tensor([[-0.7795085,  0.       ],
             [ 0.       , -0.7795085]], requires_grad=True),
    tensor([[-0.125, 0.  ],
            [0.  , -0.125]], requires_grad=True))

    The following example highlights how to make use of broadcasting gradients at the QNode level. Internally, broadcasting is used to compute the parameter-shift rule when required, which may result in performance improvements.

    @qml.qnode(dev, diff_method="parameter-shift", broadcast=True)
    def circuit(x, y):
        qml.RX(x, wires=0)
        qml.RY(y, wires=1)
        return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
    >>> x = np.array(0.1, requires_grad=True)
    >>> y = np.array(0.4, requires_grad=True)
    >>> qml.grad(circuit)(x, y)
    (array(-0.09195267), array(-0.38747287))

    Here, only 2 circuits are created internally, rather than 4 with broadcast=False.

    To illustrate the speedup, for a constant-depth circuit with Pauli rotations and controlled Pauli rotations, the time required to compute qml.gradients.param_shift(circuit, broadcast=False)(params) ("No broadcasting") and qml.gradients.param_shift(circuit, broadcast=True)(params) ("Broadcasting") as a function of the number of qubits is given here.

  • Operations for quantum chemistry now support parameter broadcasting. (#2726)

    >>> op = qml.SingleExcitation(np.array([0.3, 1.2, -0.7]), wires=[0, 1])
    >>> op.matrix().shape
    (3, 4, 4)

Intuitive operator arithmetic 🧮

  • New functionality for representing the sum, product, and scalar-product of operators is available. (#2475) (#2625) (#2622) (#2721)

    The following functionalities have been added to facilitate creating new operators whose matrix, terms, and eigenvalues can be accessed as per usual, while maintaining differentiability. Operators created from these new features can be used within QNodes as operations or as observables (where physically applicable).

    • Summing any number of operators via qml.op_sum results in a "summed" operator:

      >>> ops_to_sum = [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(0)] 
      >>> summed_ops = qml.op_sum(*ops_to_sum)
      >>> summed_ops
      PauliX(wires=[0]) + PauliY(wires=[1]) + PauliZ(wires=[0])
      >>> qml.matrix(summed_ops)
      array([[ 1.+0.j,  0.-1.j,  1.+0.j,  0.+0.j],
             [ 0.+1.j,  1.+0.j,  0.+0.j,  1.+0.j],
             [ 1.+0.j,  0.+0.j, -1.+0.j,  0.-1.j],
             [ 0.+0.j,  1.+0.j,  0.+1.j, -1.+0.j]])
      >>> summed_ops.terms()
      ([1.0, 1.0, 1.0], (PauliX(wires=[0]), PauliY(wires=[1]), PauliZ(wires=[0])))
    • Multiplying any number of operators via qml.prod results in a "product" operator, where the matrix product or tensor product is used correspondingly:

      >>> theta = 1.23
      >>> prod_op = qml.prod(qml.PauliZ(0), qml.RX(theta, 1))
      >>> prod_op
      PauliZ(wires=[0]) @ RX(1.23, wires=[1]) 
      >>> qml.eigvals(prod_op)
      [-1.39373197 -0.23981492  0.23981492  1.39373197]
    • Taking the product of a coefficient and an operator via qml.s_prod produces a "scalar-product" operator:

      >>> sprod_op = qml.s_prod(2.0, qml.PauliX(0))
      >>> sprod_op
      2.0*(PauliX(wires=[0]))
      >>> sprod_op.matrix()
      array([[ 0., 2.],
             [ 2., 0.]])
      >>> sprod_op.terms()
      ([2.0], [PauliX(wires=[0])])

    Each of these new functionalities can be used within QNodes as operators or observables, where applica...

Read more

Release 0.24.0

20 Jun 19:40
81d6095
Compare
Choose a tag to compare

New features since last release

All new quantum information quantities 📏

  • Functionality for computing quantum information quantities for QNodes has been added. (#2554) (#2569) (#2598) (#2617) (#2631) (#2640) (#2663) (#2684) (#2688) (#2695) (#2710) (#2712)

    This includes two new QNode measurements:

    • The Von Neumann entropy via qml.vn_entropy:

      >>> dev = qml.device("default.qubit", wires=2)
      >>> @qml.qnode(dev)
      ... def circuit_entropy(x):
      ...     qml.IsingXX(x, wires=[0,1])
      ...     return qml.vn_entropy(wires=[0], log_base=2)
      >>> circuit_entropy(np.pi/2)
      1.0
    • The mutual information via qml.mutual_info:

      >>> dev = qml.device("default.qubit", wires=2)
      >>> @qml.qnode(dev)
      ... def circuit(x):
      ...     qml.IsingXX(x, wires=[0,1])
      ...     return qml.mutual_info(wires0=[0], wires1=[1], log_base=2)
      >>> circuit(np.pi/2)
      2.0

    New differentiable transforms are also available in the qml.qinfo module:

    • The classical and quantum Fisher information via qml.qinfo.classical_fisher, qml.qinfo.quantum_fisher, respectively:

      dev = qml.device("default.qubit", wires=3)
      
      @qml.qnode(dev)
      def circ(params):
          qml.RY(params[0], wires=1)
          qml.CNOT(wires=(1,0))
          qml.RY(params[1], wires=1)
          qml.RZ(params[2], wires=1)
          return qml.expval(qml.PauliX(0) @ qml.PauliX(1) - 0.5 * qml.PauliZ(1))
      
      params = np.array([0.5, 1., 0.2], requires_grad=True)
      cfim = qml.qinfo.classical_fisher(circ)(params)
      qfim = qml.qinfo.quantum_fisher(circ)(params)

      These quantities are typically employed in variational optimization schemes to tilt the gradient in a more favourable direction --- producing what is known as the natural gradient. For example:

      >>> grad = qml.grad(circ)(params)
      >>> cfim @ grad  # natural gradient
      [ 5.94225615e-01 -2.61509542e-02 -1.18674655e-18]
      >>> qfim @ grad  # quantum natural gradient
      [ 0.59422561 -0.02615095 -0.03989212]
    • The fidelity between two arbitrary states via qml.qinfo.fidelity:

      dev = qml.device('default.qubit', wires=1)
      
      @qml.qnode(dev)
      def circuit_rx(x):
          qml.RX(x[0], wires=0)
          qml.RZ(x[1], wires=0)
          return qml.state()
      
      @qml.qnode(dev)
      def circuit_ry(y):
          qml.RY(y, wires=0)
          return qml.state()
      >>> x = np.array([0.1, 0.3], requires_grad=True)
      >>> y = np.array(0.2, requires_grad=True) 
      >>> fid_func = qml.qinfo.fidelity(circuit_rx, circuit_ry, wires0=[0], wires1=[0])
      >>> fid_func(x, y)
      0.9905158135644924
      >>> df = qml.grad(fid_func)
      >>> df(x, y)
      (array([-0.04768725, -0.29183666]), array(-0.09489803))
    • Reduced density matrices of arbitrary states via qml.qinfo.reduced_dm:

      dev = qml.device("default.qubit", wires=2)
      @qml.qnode(dev)
      def circuit(x):
          qml.IsingXX(x, wires=[0,1])
          return qml.state()
      >>> qml.qinfo.reduced_dm(circuit, wires=[0])(np.pi/2)
      [[0.5+0.j 0.+0.j]
        [0.+0.j 0.5+0.j]]
    • Similar transforms, qml.qinfo.vn_entropy and qml.qinfo.mutual_info exist
      for transforming QNodes.

    Currently, all quantum information measurements and transforms are differentiable, but only support statevector devices, with hardware support to come in a future release (with the exception of qml.qinfo.classical_fisher and qml.qinfo.quantum_fisher, which are both hardware compatible).

    For more information, check out the new qinfo module and measurements page.

  • In addition to the QNode transforms and measurements above, functions for computing and differentiating quantum information metrics with numerical statevectors and density matrices have been added to the qml.math module. This enables flexible custom post-processing.

    Added functions include:

    • qml.math.reduced_dm
    • qml.math.vn_entropy
    • qml.math.mutual_info
    • qml.math.fidelity

    For example:

    >>> x = torch.tensor([1.0, 0.0, 0.0, 1.0], requires_grad=True)
    >>> en = qml.math.vn_entropy(x / np.sqrt(2.), indices=[0])
    >>> en
    tensor(0.6931, dtype=torch.float64, grad_fn=<DivBackward0>)
    >>> en.backward()
    >>> x.grad
    tensor([-0.3069,  0.0000,  0.0000, -0.3069])

Faster mixed-state training with backpropagation 📉

  • The default.mixed device now supports differentiation via backpropagation with the Autograd, TensorFlow, and PyTorch (CPU) interfaces, leading to significantly more performant optimization and training. (#2615) (#2670) (#2680)

    As a result, the default differentiation method for the device is now "backprop". To continue using the old default "parameter-shift", explicitly specify this differentiation method in the QNode:

    dev = qml.device("default.mixed", wires=2)
    
    @qml.qnode(dev, interface="autograd", diff_method="backprop")
    def circuit(x):
        qml.RY(x, wires=0)
        qml.CNOT(wires=[0, 1])
        return qml.expval(qml.PauliZ(wires=1))
    >>> x = np.array(0.5, requires_grad=True)
    >>> circuit(x)
    array(0.87758256)
    >>> qml.grad(circuit)(x)
    -0.479425538604203

Support for quantum parameter broadcasting 📡

  • Quantum operators, functions, and tapes now support broadcasting across parameter dimensions, making it more convenient for developers to execute their PennyLane programs with multiple sets of parameters. (#2575) (#2609)

    Parameter broadcasting refers to passing tensor parameters with additional leading dimensions to quantum operators; additional dimensions will flow through the computation, and produce additional dimensions at the output.

    For example, instantiating a rotation gate with a one-dimensional array leads to a broadcasted Operation:

    >>> x = np.array([0.1, 0.2, 0.3], requires_grad=True)
    >>> op = qml.RX(x, 0)
    >>> op.batch_size
    3

    Its matrix correspondingly is augmented by a leading dimension of size batch_size:

    >>> np.round(qml.matrix(op), 4)
    tensor([[[0.9988+0.j    , 0.    -0.05j  ],
           [0.    -0.05j  , 0.9988+0.j    ]],
          [[0.995 +0.j    , 0.    -0.0998j],
           [0.    -0.0998j, 0.995 +0.j    ]],
          [[0.9888+0.j    , 0.    -0.1494j],
           [0.    -0.1494j, 0.9888+0.j    ]]], requires_grad=True)
    >>> qml.matrix(op).shape
    (3, 2, 2)

    This can be extended to quantum functions, where we may mix-and-match operations with batched parameters and those without. However, the batch_size of each batched Operator within the quantum function must be the same:

    >>> dev = qml.device('default.qubit', wires=1)
    >>> @qml.qnode(dev)
    ... def circuit_rx(x, z):
    ...     qml.RX(x, wires=0)
    ...     qml.RZ(z, wires=0)
    ...     qml.RY(0.3, wires=0)
    ...     return qml.probs(wires=0)
    >>> circuit_rx([0.1, 0.2], [0.3, 0.4])
    tensor([[0.97092256, 0.02907744],
            [0.95671515, 0.04328485]], requires_grad=True)

    Parameter broadcasting is supported on all devices, hardware and simulator. Note that if not natively supported by the underlying device, parameter broadcasting may result in additional quantum device evaluations.

  • A new transform, qml.transforms.broadcast_expand, has been added, which automates the process of transforming quantum functions (and tapes) to multiple quantum evaluations with no parameter broadcasting. (#2590)

    >>> dev = qml.device('default.qubit', wires=1)
    >>> @qml.transforms.broadcast_expand()
    >>> @qml.qnode(dev)
    ... def circuit_rx(x, z):
    ...     qml.RX(x, wires=0)
    ...     qml.RZ(z, wires=0)
    ...     qml.RY(0.3, wires=0)
    ...     return qml.probs(wires=0)
    >>> print(qml.draw(circuit_rx)([0.1, 0.2], [0.3, 0.4]))
    0: ──RX(0.10)──RZ(0.30)──RY(0.30)─┤  Probs
    \
    0: ──RX(0.20)──RZ(0.40)──RY(0.30)─┤  Probs

    Under-the-hood, this transform is used for devices that don't natively support parameter broadcasting.

  • To specify that a device natively supports broadcasted tapes, the new flag Device.capabilities()["supports_broadcasting"] should be set to True.

  • To support parameter broadcasting for new or custom operations, the following new Operator class at...

Read more

Release 0.23.1

09 May 15:09
6fca066
Compare
Choose a tag to compare

Bug fixes

  • Fixed a bug enabling PennyLane to work with the latest version of Autoray. (#2548)

Contributors

This release contains contributions from (in alphabetical order):

Josh Izaac.

Release 0.23.0

25 Apr 22:09
Compare
Choose a tag to compare

New features since last release

More powerful circuit cutting ✂️

  • Quantum circuit cutting (running N-wire circuits on devices with fewer than N wires) is now supported for QNodes of finite-shots using the new @qml.cut_circuit_mc transform. (#2313) (#2321) (#2332) (#2358) (#2382) (#2399) (#2407) (#2444)

    With these new additions, samples from the original circuit can be simulated using a Monte Carlo method, using fewer qubits at the expense of more device executions. Additionally, this transform can take an optional classical processing function as an argument and return an expectation value.

    The following 3-qubit circuit contains a WireCut operation and a sample measurement. When decorated with @qml.cut_circuit_mc, we can cut the circuit into two 2-qubit fragments:

    dev = qml.device("default.qubit", wires=2, shots=1000)
    
    @qml.cut_circuit_mc
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(0.89, wires=0)
        qml.RY(0.5, wires=1)
        qml.RX(1.3, wires=2)
    
        qml.CNOT(wires=[0, 1])
        qml.WireCut(wires=1)
        qml.CNOT(wires=[1, 2])
    
        qml.RX(x, wires=0)
        qml.RY(0.7, wires=1)
        qml.RX(2.3, wires=2)
        return qml.sample(wires=[0, 2])

    we can then execute the circuit as usual by calling the QNode:

    >>> x = 0.3
    >>> circuit(x)
    tensor([[1, 1],
            [0, 1],
            [0, 1],
            ...,
            [0, 1],
            [0, 1],
            [0, 1]], requires_grad=True)

    Furthermore, the number of shots can be temporarily altered when calling the QNode:

    >>> results = circuit(x, shots=123)
    >>> results.shape
    (123, 2)

    The cut_circuit_mc transform also supports returning sample-based expectation values of observables using the classical_processing_fn argument. Refer to the UsageDetails section of the transform documentation for an example.

  • The cut_circuit transform now supports automatic graph partitioning by specifying auto_cutter=True to cut arbitrary tape-converted graphs using the general purpose graph partitioning framework KaHyPar. (#2330) (#2428)

    Note that KaHyPar needs to be installed separately with the auto_cutter=True option.

    For integration with the existing low-level manual cut pipeline, refer to the documentation of the
    function
    .

    @qml.cut_circuit(auto_cutter=True)
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x, wires=0)
        qml.RY(0.9, wires=1)
        qml.RX(0.3, wires=2)
        qml.CZ(wires=[0, 1])
        qml.RY(-0.4, wires=0)
        qml.CZ(wires=[1, 2])
        return qml.expval(qml.grouping.string_to_pauli_word("ZZZ"))
    >>> x = np.array(0.531, requires_grad=True)
    >>> circuit(x)
    0.47165198882111165
    >>> qml.grad(circuit)(x)
    -0.276982865449393

Grand QChem unification ⚛️ 🏰

  • Quantum chemistry functionality --- previously split between an external pennylane-qchem package and internal qml.hf differentiable Hartree-Fock solver --- is now unified into a single, included, qml.qchem module. (#2164) (#2385) (#2352) (#2420) (#2454) (#2199) (#2371) (#2272) (#2230) (#2415) (#2426) (#2465)

    The qml.qchem module provides a differentiable Hartree-Fock solver and the functionality to construct a fully-differentiable molecular Hamiltonian.

    For example, one can continue to generate molecular Hamiltonians using qml.qchem.molecular_hamiltonian:

    symbols = ["H", "H"]
    geometry = np.array([[0., 0., -0.66140414], [0., 0., 0.66140414]])
    hamiltonian, qubits = qml.qchem.molecular_hamiltonian(symbols, geometry, method="dhf")

    By default, this will use the differentiable Hartree-Fock solver; however, simply set method="pyscf" to continue to use PySCF for Hartree-Fock calculations.

  • Functions are added for building a differentiable dipole moment observable. Functions for computing multipole moment molecular integrals, needed for building the dipole moment observable, are also added. (#2173) (#2166)

    The dipole moment observable can be constructed using qml.qchem.dipole_moment:

    symbols  = ['H', 'H']
    geometry = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
    mol = qml.qchem.Molecule(symbols, geometry)
    args = [geometry]
    D = qml.qchem.dipole_moment(mol)(*args)
  • The efficiency of computing molecular integrals and Hamiltonian is improved. This has been done by adding optimized functions for building fermionic and qubit observables and optimizing the functions used for computing the electron repulsion integrals. (#2316)

  • The 6-31G basis set is added to the qchem basis set repo. This addition allows performing differentiable Hartree-Fock calculations with basis sets beyond the minimal sto-3g basis set for atoms with atomic number 1-10. (#2372)

    The 6-31G basis set can be used to construct a Hamiltonian as

    symbols = ["H", "H"]
    geometry = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
    H, qubits = qml.qchem.molecular_hamiltonian(symbols, geometry, basis="6-31g")
  • External dependencies are replaced with local functions for spin and particle number observables. (#2197) (#2362)

Pattern matching optimization 🔎 💎

  • Added an optimization transform that matches pieces of user-provided identity templates in a circuit and replaces them with an equivalent component. (#2032)

    For example, consider the following circuit where we want to replace sequence of two pennylane.S gates with a pennylane.PauliZ gate.

    def circuit():
        qml.S(wires=0)
        qml.PauliZ(wires=0)
        qml.S(wires=1)
        qml.CZ(wires=[0, 1])
        qml.S(wires=1)
        qml.S(wires=2)
        qml.CZ(wires=[1, 2])
        qml.S(wires=2)
        return qml.expval(qml.PauliX(wires=0))

    We specify use the following pattern that implements the identity:

    with qml.tape.QuantumTape() as pattern:
        qml.S(wires=0)
        qml.S(wires=0)
        qml.PauliZ(wires=0)

    To optimize the circuit with this identity pattern, we apply the qml.transforms.pattern_matching transform.

    >>> dev = qml.device('default.qubit', wires=5)
    >>> qnode = qml.QNode(circuit, dev)
    >>> optimized_qfunc = qml.transforms.pattern_matching_optimization(pattern_tapes=[pattern])(circuit)
    >>> optimized_qnode = qml.QNode(optimized_qfunc, dev)
    >>> print(qml.draw(qnode)())
    0: ──S──Z─╭C──────────┤  <X>
    1: ──S────╰Z──S─╭C────┤
    2: ──S──────────╰Z──S─┤
    >>> print(qml.draw(optimized_qnode)())
    0: ──S⁻¹─╭C────┤  <X>
    1: ──Z───╰Z─╭C─┤
    2: ──Z──────╰Z─┤

    For more details on using pattern matching optimization you can check the corresponding documentation and also the following paper.

Measure the distance between two unitaries📏

  • Added the HilbertSchmidt and the LocalHilbertSchmidt templates to be used for computing distance measures between unitaries. (#2364)

    Given a unitary U, qml.HilberSchmidt can be used to measure the distance between unitaries and to define a cost function (cost_hst) used for learning a unitary V that is equivalent to U up to a global phase:

    # Represents unitary U
    with qml.tape.QuantumTape(do_queue=False) as u_tape:
        qml.Hadamard(wires=0)
    
    # Represents unitary V
    def v_function(params):
        qml.RZ(params[0], wires=1)
    
    @qml.qnode(dev)
    def hilbert_test(v_params, v_function, v_wires, u_tape):
        qml.HilbertSchmidt(v_params, v_function=v_function, v_wires=v_wires, u_tape=u_tape)
        return qml.probs(u_tape.wires + v_wires)
    
    def cost_hst(parameters, v_function, v_wires, u_tape):
        return (1 - hilbert_test(v_params=parameters, v_function=v_function, v_wires=v_wires, u_tape=u_tape)[0])
    >>> cost_hst(parameters=[0.1], v_function=v_function, v_wires=[...
Read more

Release 0.22.2

01 Apr 17:48
Compare
Choose a tag to compare

Bug fixes

  • Most compilation transforms, and relevant subroutines, have been updated to support just-in-time compilation with jax.jit. This fix was intended to be included in v0.22.0, but due to a bug was incomplete. (#2397)

Documentation

  • The documentation run has been updated to require jinja2==3.0.3 due to an issue that arises with jinja2 v3.1.0 and sphinx v3.5.3. (#2378)

Contributors

This release contains contributions from (in alphabetical order):

Olivia Di Matteo, Christina Lee, Romain Moyard, Antal Száva.

Release 0.22.1

16 Mar 02:44
Compare
Choose a tag to compare

Bug fixes

  • Fixes cases with qml.measure where unexpected operations were added to the circuit. (#2328)

Contributors

This release contains contributions from (in alphabetical order):

Guillermo Alonso-Linaje, Antal Száva.

Release 0.22.0

15 Mar 06:13
23ba3f3
Compare
Choose a tag to compare

New features since last release

Quantum circuit cutting ✂️

  • You can now run N-wire circuits on devices with fewer than N wires, by strategically placing WireCut operations that allow their circuit to be partitioned into smaller fragments, at a cost of needing to perform a greater number of device executions. Circuit cutting is enabled by decorating a QNode with the @qml.cut_circuit transform. (#2107) (#2124) (#2153) (#2165) (#2158) (#2169) (#2192) (#2216) (#2168) (#2223) (#2231) (#2234) (#2244) (#2251) (#2265) (#2254) (#2260) (#2257) (#2279)

    The example below shows how a three-wire circuit can be run on a two-wire device:

    dev = qml.device("default.qubit", wires=2)
    
    @qml.cut_circuit
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x, wires=0)
        qml.RY(0.9, wires=1)
        qml.RX(0.3, wires=2)
    
        qml.CZ(wires=[0, 1])
        qml.RY(-0.4, wires=0)
    
        qml.WireCut(wires=1)
    
        qml.CZ(wires=[1, 2])
    
        return qml.expval(qml.grouping.string_to_pauli_word("ZZZ"))

    Instead of executing the circuit directly, it will be partitioned into smaller fragments according to the WireCut locations, and each fragment executed multiple times. Combining the results of the fragment executions will recover the expected output of the original uncut circuit.

    >>> x = np.array(0.531, requires_grad=True)
    >>> circuit(0.531)
    0.47165198882111165

    Circuit cutting support is also differentiable:

    >>> qml.grad(circuit)(x)
    -0.276982865449393

    For more details on circuit cutting, check out the qml.cut_circuit documentation page or Peng et. al.

Conditional operations: quantum teleportation unlocked 🔓🌀

  • Support for mid-circuit measurements and conditional operations has been added, to enable use cases like quantum teleportation, quantum error correction and quantum error mitigation. (#2211) (#2236) (#2275)

    Two new functions have been added to support this capability:

    • qml.measure() places mid-circuit measurements in the middle of a quantum function.

    • qml.cond() allows operations and quantum functions to be conditioned on the result of a previous measurement.

    For example, the code below shows how to teleport a qubit from wire 0 to wire 2:

    dev = qml.device("default.qubit", wires=3)
    input_state = np.array([1, -1], requires_grad=False) / np.sqrt(2)
    
    @qml.qnode(dev)
    def teleport(state):
        # Prepare input state
        qml.QubitStateVector(state, wires=0)
    
        # Prepare Bell state
        qml.Hadamard(wires=1)
        qml.CNOT(wires=[1, 2])
    
        # Apply gates
        qml.CNOT(wires=[0, 1])
        qml.Hadamard(wires=0)
    
        # Measure first two wires
        m1 = qml.measure(0)
        m2 = qml.measure(1)
    
        # Condition final wire on results
        qml.cond(m2 == 1, qml.PauliX)(wires=2)
        qml.cond(m1 == 1, qml.PauliZ)(wires=2)
    
        # Return state on final wire
        return qml.density_matrix(wires=2)

    We can double-check that the qubit has been teleported by computing the overlap between the input state and the resulting state on wire 2:

    >>> output_state = teleport(input_state)
    >>> output_state
    tensor([[ 0.5+0.j, -0.5+0.j],
            [-0.5+0.j,  0.5+0.j]], requires_grad=True)
    >>> input_state.conj() @ output_state @ input_state
    tensor(1.+0.j, requires_grad=True)

    For a full description of new capabilities, refer to the Mid-circuit measurements and conditional operations section in the documentation.

  • Train mid-circuit measurements by deferring them, via the new @qml.defer_measurements transform. (#2211) (#2236) (#2275)

    If a device doesn't natively support mid-circuit measurements, the @qml.defer_measurements transform can be applied to the QNode to transform the QNode into one with terminal measurements and controlled operations:

    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev)
    @qml.defer_measurements
    def circuit(x):
        qml.Hadamard(wires=0)
    
        m = qml.measure(0)
    
        def op_if_true():
            return qml.RX(x**2, wires=1)
    
        def op_if_false():
            return qml.RY(x, wires=1)
    
        qml.cond(m==1, op_if_true, op_if_false)()
    
        return qml.expval(qml.PauliZ(1))
    >>> x = np.array(0.7, requires_grad=True)
    >>> print(qml.draw(circuit, expansion_strategy="device")(x))
    0: ──H─╭C─────────X─╭C─────────X─┤
    1: ────╰RX(0.49)────╰RY(0.70)────┤  <Z>
    >>> circuit(x)
    tensor(0.82358752, requires_grad=True)

    Deferring mid-circuit measurements also enables differentiation:

    >>> qml.grad(circuit)(x)
    -0.651546965338656

Debug with mid-circuit quantum snapshots 📷

  • A new operation qml.Snapshot has been added to assist in debugging quantum functions. (#2233) (#2289) (#2291) (#2315)

    qml.Snapshot saves the internal state of devices at arbitrary points of execution.

    Currently supported devices include:

    • default.qubit: each snapshot saves the quantum state vector
    • default.mixed: each snapshot saves the density matrix
    • default.gaussian: each snapshot saves the covariance matrix and vector of means

    During normal execution, the snapshots are ignored:

    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev, interface=None)
    def circuit():
        qml.Snapshot()
        qml.Hadamard(wires=0)
        qml.Snapshot("very_important_state")
        qml.CNOT(wires=[0, 1])
        qml.Snapshot()
        return qml.expval(qml.PauliX(0))

    However, when using the qml.snapshots transform, intermediate device states will be stored and returned alongside the results.

    >>> qml.snapshots(circuit)()
    {0: array([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]),
     'very_important_state': array([0.70710678+0.j, 0.        +0.j, 0.70710678+0.j, 0.        +0.j]),
     2: array([0.70710678+0.j, 0.        +0.j, 0.        +0.j, 0.70710678+0.j]),
     'execution_results': array(0.)}

Batch embedding and state preparation data 📦

  • Added the @qml.batch_input transform to enable batching non-trainable gate parameters. In addition, the qml.qnn.KerasLayer class has been updated to natively support batched training data. (#2069)

    As with other transforms, @qml.batch_input can be used to decorate QNodes:

    dev = qml.device("default.qubit", wires=2, shots=None)
    
    @qml.batch_input(argnum=0)
    @qml.qnode(dev, diff_method="parameter-shift", interface="tf")
    def circuit(inputs, weights):
        # add a batch dimension to the embedding data
        qml.AngleEmbedding(inputs, wires=range(2), rotation="Y")
        qml.RY(weights[0], wires=0)
        qml.RY(weights[1], wires=1)
        return qml.expval(qml.PauliZ(1))

    Batched input parameters can then be passed during QNode evaluation:

    >>> x = tf.random.uniform((10, 2), 0, 1)
    >>> w = tf.random.uniform((2,), 0, 1)
    >>> circuit(x, w)
    <tf.Tensor: shape=(10,), dtype=float64, numpy=
    array([0.46230079, 0.73971315, 0.95666004, 0.5355225 , 0.66180948,
            0.44519553, 0.93874261, 0.9483197 , 0.78737918, 0.90866411])>

Even more mighty quantum transforms 🐛➡🦋

  • New functions and transforms of operators have been added:

    • qml.matrix() for computing the matrix representation of one or more unitary operators. (#2241)

    • qml.eigvals() for computing the eigenvalues of one or more operators. (#2248)

    • qml.generator() for computing the generator of a single-parameter unitary operation. (#2256)

    All operator transforms can be used on instantiated operators,

    >>> op = qml.RX(0.54, wires=0)
    >>> qml.matrix(op)
    [[0.9637709+0.j         0.       -0.26673144j]
    [0.       -0.26673144j 0....
Read more

Release 0.21.0

08 Feb 07:13
323f102
Compare
Choose a tag to compare

New features since last release

Reduce qubit requirements of simulating Hamiltonians ⚛️

  • Functions for tapering qubits based on molecular symmetries have been added, following results from Setia et al. (#1966) (#1974) (#2041) (#2042)

    With this functionality, a molecular Hamiltonian and the corresponding Hartree-Fock (HF) state can be transformed to a new Hamiltonian and HF state that acts on a reduced number of qubits, respectively.

    # molecular geometry
    symbols = ["He", "H"]
    geometry = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4588684632]])
    mol = qml.hf.Molecule(symbols, geometry, charge=1)
    
    # generate the qubit Hamiltonian
    H = qml.hf.generate_hamiltonian(mol)(geometry)
    
    # determine Hamiltonian symmetries
    generators, paulix_ops = qml.hf.generate_symmetries(H, len(H.wires))
    opt_sector = qml.hf.optimal_sector(H, generators, mol.n_electrons)
    
    # taper the Hamiltonian
    H_tapered = qml.hf.transform_hamiltonian(H, generators, paulix_ops, opt_sector)

    We can compare the number of qubits required by the original Hamiltonian and the tapered Hamiltonian:

    >>> len(H.wires)
    4
    >>> len(H_tapered.wires)
    2

    For quantum chemistry algorithms, the Hartree-Fock state can also be tapered:

    n_elec = mol.n_electrons
    n_qubits = mol.n_orbitals * 2
    
    hf_tapered = qml.hf.transform_hf(
        generators, paulix_ops, opt_sector, n_elec, n_qubits
    )
    >>> hf_tapered
    tensor([1, 1], requires_grad=True)

New tensor network templates 🪢

  • Quantum circuits with the shape of a matrix product state tensor network can now be easily implemented using the new qml.MPS template, based on the work arXiv:1803.11537. (#1871)

    def block(weights, wires):
        qml.CNOT(wires=[wires[0], wires[1]])
        qml.RY(weights[0], wires=wires[0])
        qml.RY(weights[1], wires=wires[1])
    
    n_wires = 4
    n_block_wires = 2
    n_params_block = 2
    template_weights = np.array([[0.1, -0.3], [0.4, 0.2], [-0.15, 0.5]], requires_grad=True)
    
    dev = qml.device("default.qubit", wires=range(n_wires))
    
    @qml.qnode(dev)
    def circuit(weights):
        qml.MPS(range(n_wires), n_block_wires, block, n_params_block, weights)
        return qml.expval(qml.PauliZ(wires=n_wires - 1))

    The resulting circuit is:

    >>> print(qml.draw(circuit, expansion_strategy="device")(template_weights))
    0: ──╭C──RY(0.1)───────────────────────────────┤
    1: ──╰X──RY(-0.3)──╭C──RY(0.4)─────────────────┤
    2: ────────────────╰X──RY(0.2)──╭C──RY(-0.15)──┤
    3: ─────────────────────────────╰X──RY(0.5)────┤ ⟨Z⟩
  • Added a template for tree tensor networks, qml.TTN. (#2043)

    def block(weights, wires):
        qml.CNOT(wires=[wires[0], wires[1]])
        qml.RY(weights[0], wires=wires[0])
        qml.RY(weights[1], wires=wires[1])
    
    n_wires = 4
    n_block_wires = 2
    n_params_block = 2
    n_blocks = qml.MPS.get_n_blocks(range(n_wires), n_block_wires)
    template_weights = [[0.1, -0.3]] * n_blocks
    
    dev = qml.device("default.qubit", wires=range(n_wires))
    
    @qml.qnode(dev)
    def circuit(template_weights):
        qml.TTN(range(n_wires), n_block_wires, block, n_params_block, template_weights)
        return qml.expval(qml.PauliZ(wires=n_wires - 1))

    The resulting circuit is:

    >>> print(qml.draw(circuit, expansion_strategy="device")(template_weights))
    0: ──╭C──RY(0.1)─────────────────┤
    1: ──╰X──RY(-0.3)──╭C──RY(0.1)───┤
    2: ──╭C──RY(0.1)───│─────────────┤
    3: ──╰X──RY(-0.3)──╰X──RY(-0.3)──┤ ⟨Z⟩

Generalized RotosolveOptmizer 📉

  • The RotosolveOptimizer has been generalized to arbitrary frequency spectra in the cost function. Also note the changes in behaviour listed under Breaking changes. (#2081)

    Previously, the RotosolveOptimizer only supported variational circuits using special gates such as single-qubit Pauli rotations. Now, circuits with arbitrary gates are supported natively without decomposition, as long as the frequencies of the gate parameters are known. This new generalization extends the Rotosolve optimization method to a larger class of circuits, and can reduce the cost of the optimization compared to decomposing all gates to single-qubit rotations.

    Consider the QNode

    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev)
    def qnode(x, Y):
        qml.RX(2.5 * x, wires=0)
        qml.CNOT(wires=[0, 1])
        qml.RZ(0.3 * Y[0], wires=0)
        qml.CRY(1.1 * Y[1], wires=[1, 0])
        return qml.expval(qml.PauliX(0) @ qml.PauliZ(1))
    
    x = np.array(0.8, requires_grad=True)
    Y = np.array([-0.2, 1.5], requires_grad=True)

    Its frequency spectra can be easily obtained via qml.fourier.qnode_spectrum:

    >>> spectra = qml.fourier.qnode_spectrum(qnode)(x, Y)
    >>> spectra
    {'x': {(): [-2.5, 0.0, 2.5]},
     'Y': {(0,): [-0.3, 0.0, 0.3], (1,): [-1.1, -0.55, 0.0, 0.55, 1.1]}}

    We may then initialize the RotosolveOptimizer and minimize the QNode cost function by providing this information about the frequency spectra. We also compare the cost at each step to the initial cost.

    >>> cost_init = qnode(x, Y)
    >>> opt = qml.RotosolveOptimizer()
    >>> for _ in range(2):
    ...     x, Y = opt.step(qnode, x, Y, spectra=spectra)
    ...     print(f"New cost: {np.round(qnode(x, Y), 3)}; Initial cost: {np.round(cost_init, 3)}")
    New cost: 0.0; Initial cost: 0.706
    New cost: -1.0; Initial cost: 0.706

    The optimization with RotosolveOptimizer is performed in substeps. The minimal cost of these substeps can be retrieved by setting full_output=True.

    >>> x = np.array(0.8, requires_grad=True)
    >>> Y = np.array([-0.2, 1.5], requires_grad=True)
    >>> opt = qml.RotosolveOptimizer()
    >>> for _ in range(2):
    ...     (x, Y), history = opt.step(qnode, x, Y, spectra=spectra, full_output=True)
    ...     print(f"New cost: {np.round(qnode(x, Y), 3)} reached via substeps {np.round(history, 3)}")
    New cost: 0.0 reached via substeps [-0.  0.  0.]
    New cost: -1.0 reached via substeps [-1. -1. -1.]

    However, note that these intermediate minimal values are evaluations of the reconstructions that Rotosolve creates and uses internally for the optimization, and not of the original objective function. For noisy cost functions, these intermediate evaluations may differ significantly from evaluations of the original cost function.

Improved JAX support 💻

  • The JAX interface now supports evaluating vector-valued QNodes. (#2110)

    Vector-valued QNodes include those with:

    • qml.probs;
    • qml.state;
    • qml.sample or
    • multiple qml.expval / qml.var measurements.

    Consider a QNode that returns basis-state probabilities:

    dev = qml.device('default.qubit', wires=2)
    x = jnp.array(0.543)
    y = jnp.array(-0.654)
    
    @qml.qnode(dev, diff_method="parameter-shift", interface="jax")
    def circuit(x, y):
        qml.RX(x, wires=[0])
        qml.RY(y, wires=[1])
        qml.CNOT(wires=[0, 1])
        return qml.probs(wires=[1])

    The QNode can be evaluated and its jacobian can be computed:

    >>> circuit(x, y)
    DeviceArray([0.8397495 , 0.16025047], dtype=float32)
    >>> jax.jacobian(circuit, argnums=[0, 1])(x, y)
    (DeviceArray([-0.2050439,  0.2050439], dtype=float32, weak_type=True),
     DeviceArray([ 0.26043, -0.26043], dtype=float32, weak_type=True))

    Note that jax.jit is not yet supported for vector-valued QNodes.

Speedier quantum natural gradient ⚡

  • A new function for computing the metric tensor on simulators, qml.adjoint_metric_tensor, has been added, that uses classically efficient methods to massively improve performance. (#1992)

    This method, detailed in Jones (2020), computes the metric tensor using four copies of the state vector and a number of operations that scales quadratically in the number of trainable parameters.

    Note that as it makes use of state cloning, it is inherently classical and can only be used with statevector simulators and shots=None.

    It is particularly useful for larger circuits for which backpropagation requires inconvenient or even unfeasible amounts of storage, but is slower. Furthermore, the adjoint method is only available for analytic computation, not for measurements simulation with shots!=None.

    dev = qml.device("default.qubit", wires=3)
    
    @qml.qnode(dev)
    def circuit(x, y):
        qml.Rot(*x[0], wires=0)
        qml.Rot(*x[1], wires=1)
        qml.Rot(*x[2], wires=2)
        qml.CNOT(wires=[0, 1])
        qml.CNOT(wires=[1, 2])
        qml.CNOT(wires=[2, 0])
        qml.RY(y[0], wires=0)
        qml.RY(y[1], wires=1)
        qml.RY(y[0], wires=2)
        return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)), qml.expval(qml.PauliY(1))
    
    x = np.array([[0.2, 0.4, -0.1], [-2.1, 0.5, -0.2], [0.1, 0.7, -0.6]], requires_grad=False)
    y = np.array([1.3, 0.2], requires_grad=True)
    >>> qml.adjoint_metric_tensor(circuit)(x, y)
    tensor([[ 0.25495723, -0.07086695],
            [-0.07086695,  0.24945606]], requires_grad=True)

    Computational cost

    The adjoint method uses :math:2P^2+4P+1 gates ...

Read more

Release 0.20.0

14 Dec 07:47
8fdf587
Compare
Choose a tag to compare

New features since last release

Shiny new circuit drawer!🎨🖌️

  • PennyLane now supports drawing a QNode with matplotlib! (#1803) (#1811) (#1931) (#1954)

    dev = qml.device("default.qubit", wires=4)
    
    @qml.qnode(dev)
    def circuit(x, z):
        qml.QFT(wires=(0,1,2,3))
        qml.Toffoli(wires=(0,1,2))
        qml.CSWAP(wires=(0,2,3))
        qml.RX(x, wires=0)
        qml.CRZ(z, wires=(3,0))
        return qml.expval(qml.PauliZ(0))
    fig, ax = qml.draw_mpl(circuit)(1.2345, 1.2345)
    fig.show()

New and improved quantum-aware optimizers

  • Added qml.LieAlgebraOptimizer, a new quantum-aware Lie Algebra optimizer that allows one to perform gradient descent on the special unitary group. (#1911)

    dev = qml.device("default.qubit", wires=2)
    H = -1.0 * qml.PauliX(0) - qml.PauliZ(1) - qml.PauliY(0) @ qml.PauliX(1)
    
    @qml.qnode(dev)
    def circuit():
        qml.RX(0.1, wires=[0])
        qml.RY(0.5, wires=[1])
        qml.CNOT(wires=[0,1])
        qml.RY(0.6, wires=[0])
        return qml.expval(H)
    opt = qml.LieAlgebraOptimizer(circuit=circuit, stepsize=0.1)

    Note that, unlike other optimizers, the LieAlgebraOptimizer accepts a QNode with no parameters, and instead grows the circuit by pending operations during the optimization:

    >>> circuit()
    tensor(-1.3351865, requires_grad=True)
    >>> circuit1, cost = opt.step_and_cost()
    >>> circuit1()
    tensor(-1.99378872, requires_grad=True)

    For more details, see the LieAlgebraOptimizer documentation.

  • The qml.metric_tensor transform can now be used to compute the full tensor, beyond the block diagonal approximation. (#1725)

    This is performed using Hadamard tests, and requires an additional wire on the device to execute the circuits produced by the transform, as compared to the number of wires required by the original circuit. The transform defaults to computing the full tensor, which can be controlled by the approx keyword argument.

    As an example, consider the QNode

    dev = qml.device("default.qubit", wires=3)
    
    @qml.qnode(dev)
    def circuit(weights):
        qml.RX(weights[0], wires=0)
        qml.RY(weights[1], wires=0)
        qml.CNOT(wires=[0, 1])
        qml.RZ(weights[2], wires=1)
        return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
    
    weights = np.array([0.2, 1.2, -0.9], requires_grad=True)

    Then we can compute the (block) diagonal metric tensor as before, now using the approx="block-diag" keyword:

    >>> qml.metric_tensor(circuit, approx="block-diag")(weights)
    [[0.25       0.         0.        ]
     [0.         0.24013262 0.        ]
     [0.         0.         0.21846983]]

    Instead, we now can also compute the full metric tensor, using Hadamard tests on the additional wire of the device:

    >>> qml.metric_tensor(circuit)(weights)
    [[ 0.25        0.         -0.23300977]
     [ 0.          0.24013262  0.01763859]
     [-0.23300977  0.01763859  0.21846983]]

    See the metric tensor documentation for more information and usage details.

Faster performance with optimized quantum workflows

  • The QNode has been re-written to support batch execution across the board, custom gradients, better decomposition strategies, and higher-order derivatives. (#1807) (#1969)

    • Internally, if multiple circuits are generated for simultaneous execution, 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 or simulator devices with parallelization capabilities.

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

      @qml.gradients.gradient_transform
      def my_gradient_transform(tape):
          ...
          return tapes, processing_fn
      
      @qml.qnode(dev, diff_method=my_gradient_transform)
      def circuit():

    For breaking changes related to the use of the new QNode, refer to the Breaking Changes section.

    Note that the old QNode remains accessible at @qml.qnode_old.qnode, however this will be removed in the next release.

  • Custom decompositions can now be applied to operations at the device level. (#1900)

    For example, suppose we would like to implement the following QNode:

    def circuit(weights):
        qml.BasicEntanglerLayers(weights, wires=[0, 1, 2])
        return qml.expval(qml.PauliZ(0))
    
    original_dev = qml.device("default.qubit", wires=3)
    original_qnode = qml.QNode(circuit, original_dev)
    >>> weights = np.array([[0.4, 0.5, 0.6]])
    >>> print(qml.draw(original_qnode, expansion_strategy="device")(weights))
     0: ──RX(0.4)──╭C──────╭X──┤ ⟨Z⟩
     1: ──RX(0.5)──╰X──╭C──│───┤
     2: ──RX(0.6)──────╰X──╰C──┤

    Now, let's swap out the decomposition of the CNOT gate into CZ and Hadamard, and furthermore the decomposition of Hadamard into RZ and RY rather than the decomposition already available in PennyLane. We define the two decompositions like so, and pass them to a device:

    def custom_cnot(wires):
        return [
            qml.Hadamard(wires=wires[1]),
            qml.CZ(wires=[wires[0], wires[1]]),
            qml.Hadamard(wires=wires[1])
        ]
    
    def custom_hadamard(wires):
        return [
            qml.RZ(np.pi, wires=wires),
            qml.RY(np.pi / 2, wires=wires)
        ]
    
    # Can pass the operation itself, or a string
    custom_decomps = {qml.CNOT : custom_cnot, "Hadamard" : custom_hadamard}
    
    decomp_dev = qml.device("default.qubit", wires=3, custom_decomps=custom_decomps)
    decomp_qnode = qml.QNode(circuit, decomp_dev)

    Now when we draw or run a QNode on this device, the gates will be expanded according to our specifications:

    >>> print(qml.draw(decomp_qnode, expansion_strategy="device")(weights))
     0: ──RX(0.4)──────────────────────╭C──RZ(3.14)──RY(1.57)──────────────────────────╭Z──RZ(3.14)──RY(1.57)──┤ ⟨Z⟩
     1: ──RX(0.5)──RZ(3.14)──RY(1.57)──╰Z──RZ(3.14)──RY(1.57)──╭C──────────────────────│───────────────────────┤
     2: ──RX(0.6)──RZ(3.14)──RY(1.57)──────────────────────────╰Z──RZ(3.14)──RY(1.57)──╰C──────────────────────┤

    A separate context manager, set_decomposition, has also been implemented to enable application of custom decompositions on devices that have already been created.

    >>> with qml.transforms.set_decomposition(custom_decomps, original_dev):
    ...     print(qml.draw(original_qnode, expansion_strategy="device")(weights))
     0: ──RX(0.4)──────────────────────╭C──RZ(3.14)──RY(1.57)──────────────────────────╭Z──RZ(3.14)──RY(1.57)──┤ ⟨Z⟩
     1: ──RX(0.5)──RZ(3.14)──RY(1.57)──╰Z──RZ(3.14)──RY(1.57)──╭C──────────────────────│───────────────────────┤
     2: ──RX(0.6)──RZ(3.14)──RY(1.57)──────────────────────────╰Z──RZ(3.14)──RY(1.57)──╰C──────────────────────┤
  • Given an operator of the form :math:U=e^{iHt}, where :math:H has commuting terms and known eigenvalues, qml.gradients.generate_shift_rule computes the generalized parameter shift rules for determining the gradient of the expectation value :math:f(t) = \langle 0|U(t)^\dagger \hat{O} U(t)|0\rangle on hardware. (#1788) (#1932)

    Given $H = \sum_i a_i h_i$, where the eigenvalues of :math:H are known and all :math:h_i commute, we can compute the frequencies (the unique positive differences of any two eigenvalues) using qml.gradients.eigvals_to_frequencies.

    qml.gradients.generate_shift_rule can then be used to compute the parameter shift rules to compute :math:f'(t) using 2R shifted cost function evaluations. This becomes cheaper than the standard application of the chain rule and two-term shift rule when R is less than the number of Pauli words in the generator.

    For example, consider the case where :math:H has eigenspectrum (-1, 0, 1):

    >>> frequencies = qml.gradients.eigvals_to_frequencies((-1, 0, 1))
    >>> frequencies
    (1, 2)
    >>> coeffs, shifts = qml.gradients.generate_shift_rule(frequencies)
    >>> coeffs
    array([ 0.85355339, -0.85355339, -0.14644661,  0.14644661])
    >>> shifts
    array([ 0.78539816, -0.78539816,  2.35619449, -2.35619449])

    As we can see, generate_shift_rule returns four coefficients :math:c_i and shifts :math:s_i corresponding to a four term parameter shift rule. The gradient can then be reconstructed via:

    .. math:: \frac{\partial}{\partial\phi}f = \sum_{i} c_i f(\phi + s_i),

    where :math:f(\phi) = \langle 0|U(\phi)^\dagger \hat{O} U(\phi)|0\rangle for some observable :math:\hat{O} and the unitary :math:U(\phi)=e^{iH\phi}.

Support for TensorFlow AutoGraph mode with quantum hardware

  • It is now possible to use TensorFlow's AutoGraph
    mode
    with QNodes on all devices and with arbitrary
    differentiation methods. Previously, AutoGraph mode only support `di...
Read more