Release notes

This page contains the release notes for PennyLane.

orphan

Release 0.33.0 (current release)

New features since last release

Postselection and statistics in mid-circuit measurements 📌

  • It is now possible to request postselection on a mid-circuit measurement. (#4604)

    This can be achieved by specifying the postselect keyword argument in qml.measure as either 0 or 1, corresponding to the basis states.

    import pennylane as qml
    
    dev = qml.device("default.qubit")
    
    @qml.qnode(dev, interface=None)
    def circuit():
        qml.Hadamard(wires=0)
        qml.CNOT(wires=[0, 1])
        qml.measure(0, postselect=1)
        return qml.expval(qml.PauliZ(1)), qml.sample(wires=1)
    

    This circuit prepares the \(| \Phi^{+} \rangle\) Bell state and postselects on measuring \(|1\rangle\) in wire 0. The output of wire 1 is then also \(|1\rangle\) at all times:

    >>> circuit(shots=10)
    (-1.0, array([1, 1, 1, 1, 1, 1]))
    

    Note that the number of shots is less than the requested amount because we have thrown away the samples where \(|0\rangle\) was measured in wire 0.

  • Measurement statistics can now be collected for mid-circuit measurements. (#4544)

    dev = qml.device("default.qubit")
    
    @qml.qnode(dev)
    def circ(x, y):
        qml.RX(x, wires=0)
        qml.RY(y, wires=1)
        m0 = qml.measure(1)
        return qml.expval(qml.PauliZ(0)), qml.expval(m0), qml.sample(m0)
    
    >>> circ(1.0, 2.0, shots=10000)
    (0.5606, 0.7089, array([0, 1, 1, ..., 1, 1, 1]))
    

    Support is provided for both finite-shot and analytic modes and devices default to using the deferred measurement principle to enact the mid-circuit measurements.

Exponentiate Hamiltonians with flexible Trotter products 🐖

  • Higher-order Trotter-Suzuki methods are now easily accessible through a new operation called TrotterProduct. (#4661)

    Trotterization techniques are an affective route towards accurate and efficient Hamiltonian simulation. The Suzuki-Trotter product formula allows for the ability to express higher-order approximations to the matrix exponential of a Hamiltonian, and it is now available to use in PennyLane via the TrotterProduct operation. Simply specify the order of the approximation and the evolution time.

    coeffs = [0.25, 0.75]
    ops = [qml.PauliX(0), qml.PauliZ(0)]
    H = qml.dot(coeffs, ops)
    
    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev)
    def circuit():
        qml.Hadamard(0)
        qml.TrotterProduct(H, time=2.4, order=2)
        return qml.state()
    
    >>> circuit()
    [-0.13259524+0.59790098j  0.        +0.j         -0.13259524-0.77932754j  0.        +0.j        ]
    
  • Approximating matrix exponentiation with random product formulas, qDrift, is now available with the new QDrift operation. (#4671)

    As shown in 1811.08017, qDrift is a Markovian process that can provide a speedup in Hamiltonian simulation. At a high level, qDrift works by randomly sampling from the Hamiltonian terms with a probability that depends on the Hamiltonian coefficients. This method for Hamiltonian simulation is now ready to use in PennyLane with the QDrift operator. Simply specify the evolution time and the number of samples drawn from the Hamiltonian, n:

    coeffs = [0.25, 0.75]
    ops = [qml.PauliX(0), qml.PauliZ(0)]
    H = qml.dot(coeffs, ops)
    
    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev)
    def circuit():
        qml.Hadamard(0)
        qml.QDrift(H, time=1.2, n = 10)
        return qml.probs()
    
    >>> circuit()
    array([0.61814334, 0.        , 0.38185666, 0.        ])
    

Building blocks for quantum phase estimation 🧱

  • A new operator called CosineWindow has been added to prepare an initial state based on a cosine wave function. (#4683)

    As outlined in 2110.09590, the cosine tapering window is part of a modification to quantum phase estimation that can provide a cubic improvement to the algorithm’s error rate. Using CosineWindow will prepare a state whose amplitudes follow a cosinusoidal distribution over the computational basis.

    import matplotlib.pyplot as plt
    
    dev = qml.device('default.qubit', wires=4)
    
    @qml.qnode(dev)
    def example_circuit():
          qml.CosineWindow(wires=range(4))
          return qml.state()
    output = example_circuit()
    
    plt.style.use("pennylane.drawer.plot")
    plt.bar(range(len(output)), output)
    plt.show()
    

  • Controlled gate sequences raised to decreasing powers, a sub-block in quantum phase estimation, can now be created with the new ControlledSequence operator. (#4707)

    To use ControlledSequence, specify the controlled unitary operator and the control wires, control:

    dev = qml.device("default.qubit", wires = 4)
    
    @qml.qnode(dev)
    def circuit():
        for i in range(3):
            qml.Hadamard(wires = i)
        qml.ControlledSequence(qml.RX(0.25, wires = 3), control = [0, 1, 2])
        qml.adjoint(qml.QFT)(wires = range(3))
        return qml.probs(wires = range(3))
    
    >>> print(circuit())
    [0.92059345 0.02637178 0.00729619 0.00423258 0.00360545 0.00423258 0.00729619 0.02637178]
    

New device capabilities, integration with Catalyst, and more! ⚗️

  • default.qubit now uses the new qml.devices.Device API and functionality in qml.devices.qubit. If you experience any issues with the updated default.qubit, please let us know by posting an issue. The old version of the device is still accessible by the short name default.qubit.legacy, or directly via qml.devices.DefaultQubitLegacy. (#4594) (#4436) (#4620) (#4632)

    This changeover has a number of benefits for default.qubit, including:

    • The number of wires is now optional — simply having qml.device("default.qubit") is valid! If wires are not provided at instantiation, the device automatically infers the required number of wires for each circuit provided for execution.

      dev = qml.device("default.qubit")
      
      @qml.qnode(dev)
      def circuit():
          qml.PauliZ(0)
          qml.RZ(0.1, wires=1)
          qml.Hadamard(2)
          return qml.state()
      
      >>> print(qml.draw(circuit)())
      0: ──Z────────┤  State
      1: ──RZ(0.10)─┤  State
      2: ──H────────┤  State
      
    • default.qubit is no longer silently swapped out with an interface-appropriate device when the backpropagation differentiation method is used. For example, consider:

      import jax
      
      dev = qml.device("default.qubit", wires=1)
      
      @qml.qnode(dev, diff_method="backprop")
      def f(x):
          qml.RX(x, wires=0)
          return qml.expval(qml.PauliZ(0))
      
      f(jax.numpy.array(0.2))
      

      In previous versions of PennyLane, the device will be swapped for the JAX equivalent:

      >>> f.device
      <DefaultQubitJax device (wires=1, shots=None) at 0x7f8c8bff50a0>
      >>> f.device == dev
      False
      

      Now, default.qubit can itself dispatch to all the interfaces in a backprop-compatible way and hence does not need to be swapped:

      >>> f.device
      <default.qubit device (wires=1) at 0x7f20d043b040>
      >>> f.device == dev
      True
      
  • A QNode that has been decorated with qjit from PennyLane’s Catalyst library for just-in-time hybrid compilation is now compatible with qml.draw. (#4609)

    import catalyst
    
    @catalyst.qjit
    @qml.qnode(qml.device("lightning.qubit", wires=3))
    def circuit(x, y, z, c):
        """A quantum circuit on three wires."""
    
        @catalyst.for_loop(0, c, 1)
        def loop(i):
            qml.Hadamard(wires=i)
    
        qml.RX(x, wires=0)
        loop()
        qml.RY(y, wires=1)
        qml.RZ(z, wires=2)
        return qml.expval(qml.PauliZ(0))
    
    draw = qml.draw(circuit, decimals=None)(1.234, 2.345, 3.456, 1)
    
    >>> print(draw)
    0: ──RX──H──┤  <Z>
    1: ──H───RY─┤
    2: ──RZ─────┤
    

Improvements 🛠

More PyTrees!

  • MeasurementProcess and QuantumScript objects are now registered as JAX PyTrees. (#4607) (#4608)

    It is now possible to JIT-compile functions with arguments that are a MeasurementProcess or a QuantumScript:

    import jax
    
    tape0 = qml.tape.QuantumTape([qml.RX(1.0, 0), qml.RY(0.5, 0)], [qml.expval(qml.PauliZ(0))])
    dev = qml.device('lightning.qubit', wires=5)
    
    execute_kwargs = {"device": dev, "gradient_fn": qml.gradients.param_shift, "interface":"jax"}
    
    jitted_execute = jax.jit(qml.execute, static_argnames=execute_kwargs.keys())
    jitted_execute((tape0, ), **execute_kwargs)
    

Improving QChem and existing algorithms

  • Computationally expensive functions in integrals.py, electron_repulsion and _hermite_coulomb, have been modified to replace indexing with slicing for better compatibility with JAX. (#4685)

  • qml.qchem.import_state has been extended to import more quantum chemistry wavefunctions, from MPS, DMRG and SHCI classical calculations performed with the Block2 and Dice libraries. #4523 #4524 #4626 #4634

    Check out our how-to guide to learn more about how PennyLane integrates with your favourite quantum chemistry libraries.

  • The qchem fermionic_dipole and particle_number functions have been updated to use a FermiSentence. The deprecated features for using tuples to represent fermionic operations are removed. (#4546) (#4556)

  • The tensor-network template qml.MPS now supports changing the offset between subsequent blocks for more flexibility. (#4531)

  • Builtin types support with qml.pauli_decompose have been improved. (#4577)

  • AmplitudeEmbedding now inherits from StatePrep, allowing for it to not be decomposed when at the beginning of a circuit, thus behaving like StatePrep. (#4583)

  • qml.cut_circuit is now compatible with circuits that compute the expectation values of Hamiltonians with two or more terms. (#4642)

Next-generation device API

  • default.qubit now tracks the number of equivalent qpu executions and total shots when the device is sampling. Note that "simulations" denotes the number of simulation passes, whereas "executions" denotes how many different computational bases need to be sampled in. Additionally, the new default.qubit tracks the results of device.execute. (#4628) (#4649)

  • DefaultQubit can now accept a jax.random.PRNGKey as a seed to set the key for the JAX pseudo random number generator when using the JAX interface. This corresponds to the prng_key on default.qubit.jax in the old API. (#4596)

  • The JacobianProductCalculator abstract base class and implementations TransformJacobianProducts DeviceDerivatives, and DeviceJacobianProducts have been added to pennylane.interfaces.jacobian_products. (#4435) (#4527) (#4637)

  • DefaultQubit dispatches to a faster implementation for applying ParametrizedEvolution to a state when it is more efficient to evolve the state than the operation matrix. (#4598) (#4620)

  • Wires can be provided to the new device API. (#4538) (#4562)

  • qml.sample() in the new device API now returns a np.int64 array instead of np.bool8. (#4539)

  • The new device API now has a repr() method. (#4562)

  • DefaultQubit now works as expected with measurement processes that don’t specify wires. (#4580)

  • Various improvements to measurements have been made for feature parity between default.qubit.legacy and the new DefaultQubit. This includes not trying to squeeze batched CountsMP results and implementing MutualInfoMP.map_wires. (#4574)

  • devices.qubit.simulate now accepts an interface keyword argument. If a QNode with DefaultQubit specifies an interface, the result will be computed with that interface. (#4582)

  • ShotAdaptiveOptimizer has been updated to pass shots to QNode executions instead of overriding device shots before execution. This makes it compatible with the new device API. (#4599)

  • pennylane.devices.preprocess now offers the transforms decompose, validate_observables, validate_measurements, validate_device_wires, validate_multiprocessing_workers, warn_about_trainable_observables, and no_sampling to assist in constructing devices under the new device API. (#4659)

  • Updated qml.device, devices.preprocessing and the tape_expand.set_decomposition context manager to bring DefaultQubit to feature parity with default.qubit.legacy with regards to using custom decompositions. The DefaultQubit device can now be included in a set_decomposition context or initialized with a custom_decomps dictionary, as well as a custom max_depth for decomposition. (#4675)

Other improvements

  • The StateMP measurement now accepts a wire order (e.g., a device wire order). The process_state method will re-order the given state to go from the inputted wire-order to the process’s wire-order. If the process’s wire-order contains extra wires, it will assume those are in the zero-state. (#4570) (#4602)

  • Methods called add_transform and insert_front_transform have been added to TransformProgram. (#4559)

  • Instances of the TransformProgram class can now be added together. (#4549)

  • Transforms can now be applied to devices following the new device API. (#4667)

  • All gradient transforms have been updated to the new transform program system. (#4595)

  • Multi-controlled operations with a single-qubit special unitary target can now automatically decompose. (#4697)

  • pennylane.defer_measurements will now exit early if the input does not contain mid circuit measurements. (#4659)

  • The density matrix aspects of StateMP have been split into their own measurement process called DensityMatrixMP. (#4558)

  • StateMeasurement.process_state now assumes that the input is flat. ProbabilityMP.process_state has been updated to reflect this assumption and avoid redundant reshaping. (#4602)

  • qml.exp returns a more informative error message when decomposition is unavailable for non-unitary operators. (#4571)

  • Added qml.math.get_deep_interface to get the interface of a scalar hidden deep in lists or tuples. (#4603)

  • Updated qml.math.ndim and qml.math.shape to work with built-in lists or tuples that contain interface-specific scalar dat (e.g., [(tf.Variable(1.1), tf.Variable(2.2))]). (#4603)

  • When decomposing a unitary matrix with one_qubit_decomposition and opting to include the GlobalPhase in the decomposition, the phase is no longer cast to dtype=complex. (#4653)

  • _qfunc_output has been removed from QuantumScript, as it is no longer necessary. There is still a _qfunc_output property on QNode instances. (#4651)

  • qml.data.load properly handles parameters that come after 'full' (#4663)

  • The qml.jordan_wigner function has been modified to optionally remove the imaginary components of the computed qubit operator, if imaginary components are smaller than a threshold. (#4639)

  • qml.data.load correctly performs a full download of the dataset after a partial download of the same dataset has already been performed. (#4681)

  • The performance of qml.data.load() has been improved when partially loading a dataset (#4674)

  • Plots generated with the pennylane.drawer.plot style of matplotlib.pyplot now have black axis labels and are generated at a default DPI of 300. (#4690)

  • Shallow copies of the QNode now also copy the execute_kwargs and transform program. When applying a transform to a QNode, the new qnode is only a shallow copy of the original and thus keeps the same device. (#4736)

  • QubitDevice and CountsMP are updated to disregard samples containing failed hardware measurements (record as np.NaN) when tallying samples, rather than counting failed measurements as ground-state measurements, and to display qml.counts coming from these hardware devices correctly. (#4739)

Breaking changes 💔

  • qml.defer_measurements now raises an error if a transformed circuit measures qml.probs, qml.sample, or qml.counts without any wires or observable, or if it measures qml.state. (#4701)

  • The device test suite now converts device keyword arguments to integers or floats if possible. (#4640)

  • MeasurementProcess.eigvals() now raises an EigvalsUndefinedError if the measurement observable does not have eigenvalues. (#4544)

  • The __eq__ and __hash__ methods of Operator and MeasurementProcess no longer rely on the object’s address in memory. Using == with operators and measurement processes will now behave the same as qml.equal, and objects of the same type with the same data and hyperparameters will have the same hash. (#4536)

    In the following scenario, the second and third code blocks show the previous and current behaviour of operator and measurement process equality, determined by ==:

    op1 = qml.PauliX(0)
    op2 = qml.PauliX(0)
    op3 = op1
    

    Old behaviour:

    >>> op1 == op2
    False
    >>> op1 == op3
    True
    

    New behaviour:

    >>> op1 == op2
    True
    >>> op1 == op3
    True
    

    The __hash__ dunder method defines the hash of an object. The default hash of an object is determined by the objects memory address. However, the new hash is determined by the properties and attributes of operators and measurement processes. Consider the scenario below. The second and third code blocks show the previous and current behaviour.

    op1 = qml.PauliX(0)
    op2 = qml.PauliX(0)
    

    Old behaviour:

    >>> print({op1, op2})
    {PauliX(wires=[0]), PauliX(wires=[0])}
    

    New behaviour:

    >>> print({op1, op2})
    {PauliX(wires=[0])}
    
  • The old return type and associated functions qml.enable_return and qml.disable_return have been removed. (#4503)

  • The mode keyword argument in QNode has been removed. Please use grad_on_execution instead. (#4503)

  • The CV observables qml.X and qml.P have been removed. Please use qml.QuadX and qml.QuadP instead. (#4533)

  • The sampler_seed argument of qml.gradients.spsa_grad has been removed. Instead, the sampler_rng argument should be set, either to an integer value, which will be used to create a PRNG internally, or to a NumPy pseudo-random number generator (PRNG) created via np.random.default_rng(seed). (#4550)

  • The QuantumScript.set_parameters method and the QuantumScript.data setter have been removed. Please use QuantumScript.bind_new_parameters instead. (#4548)

  • The method tape.unwrap() and corresponding UnwrapTape and Unwrap classes have been removed. Instead of tape.unwrap(), use qml.transforms.convert_to_numpy_parameters. (#4535)

  • The RandomLayers.compute_decomposition keyword argument ratio_imprivitive has been changed to ratio_imprim to match the call signature of the operation. (#4552)

  • The private TmpPauliRot operator used for SpecialUnitary no longer decomposes to nothing when the theta value is trainable. (#4585)

  • ProbabilityMP.marginal_prob has been removed. Its contents have been moved into process_state, which effectively just called marginal_prob with np.abs(state) ** 2. (#4602)

Deprecations 👋

  • The following decorator syntax for transforms has been deprecated and will raise a warning: (#4457)

    @transform_fn(**transform_kwargs)
    @qml.qnode(dev)
    def circuit():
        ...
    

    If you are using a transform that has supporting transform_kwargs, please call the transform directly using circuit = transform_fn(circuit, **transform_kwargs), or use functools.partial:

    @functools.partial(transform_fn, **transform_kwargs)
    @qml.qnode(dev)
    def circuit():
        ...
    
  • The prep keyword argument in QuantumScript has been deprecated and will be removed from QuantumScript. StatePrepBase operations should be placed at the beginning of the ops list instead. (#4554)

  • qml.gradients.pulse_generator has been renamed to qml.gradients.pulse_odegen to adhere to paper naming conventions. During v0.33, pulse_generator is still available but raises a warning. (#4633)

Documentation 📝

  • A warning section in the docstring for DefaultQubit regarding the start method used in multiprocessing has been added. This may help users circumvent issues arising in Jupyter notebooks on macOS for example. (#4622)

  • Documentation improvements to the new device API have been made. The documentation now correctly states that interface-specific parameters are only passed to the device for backpropagation derivatives. (#4542)

  • Functions for qubit-simulation to the qml.devices sub-page of the “Internal” section have been added. Note that these functions are unstable while device upgrades are underway. (#4555)

  • A documentation improvement to the usage example in the qml.QuantumMonteCarlo page has been made. An integral was missing the differential \(dx\). (#4593)

  • A documentation improvement for the use of the pennylane style of qml.drawer and the pennylane.drawer.plot style of matplotlib.pyplot has been made by clarifying the use of the default font. (#4690)

Bug fixes 🐛

  • Jax jit now works when a probability measurement is broadcasted onto all wires. (#4742)

  • Fixed LocalHilbertSchmidt.compute_decomposition so that the template can be used in a QNode. (#4719)

  • Fixes transforms.transpile with arbitrary measurement processes. (#4732)

  • Providing work_wires=None to qml.GroverOperator no longer interprets None as a wire. (#4668)

  • Fixed an issue where the __copy__ method of the qml.Select() operator attempted to access un-initialized data. (#4551)

  • Fixed the skip_first option in expand_tape_state_prep. (#4564)

  • convert_to_numpy_parameters now uses qml.ops.functions.bind_new_parameters. This reinitializes the operation and makes sure everything references the new NumPy parameters. (#4540)

  • tf.function no longer breaks ProbabilityMP.process_state, which is needed by new devices. (#4470)

  • Fixed unit tests for qml.qchem.mol_data. (#4591)

  • Fixed ProbabilityMP.process_state so that it allows for proper Autograph compilation. Without this, decorating a QNode that returns an expval with tf.function would fail when computing the expectation. (#4590)

  • The torch.nn.Module properties are now accessible on a pennylane.qnn.TorchLayer. (#4611)

  • qml.math.take with Pytorch now returns tensor[..., indices] when the user requests the last axis (axis=-1). Without the fix, it would wrongly return tensor[indices]. (#4605)

  • Ensured the logging TRACE level works with gradient-free execution. (#4669)

Contributors ✍️

This release contains contributions from (in alphabetical order):

Guillermo Alonso, Utkarsh Azad, Thomas Bromley, Isaac De Vlugt, Jack Brown, Stepan Fomichev, Joana Fraxanet, Diego Guala, Soran Jahangiri, Edward Jiang, Korbinian Kottmann, Ivana Kurečić Christina Lee, Lillian M. A. Frederiksen, Vincent Michaud-Rioux, Romain Moyard, Daniel F. Nino, Lee James O’Riordan, Mudit Pandey, Matthew Silverman, Jay Soni.

orphan

Release 0.32.0

New features since last release

Encode matrices using a linear combination of unitaries ⛓️️

  • It is now possible to encode an operator A into a quantum circuit by decomposing it into a linear combination of unitaries using PREP (qml.StatePrep) and SELECT (qml.Select) routines. (#4431) (#4437) (#4444) (#4450) (#4506) (#4526)

    Consider an operator A composed of a linear combination of Pauli terms:

    >>> A = qml.PauliX(2) + 2 * qml.PauliY(2) + 3 * qml.PauliZ(2)
    

    A decomposable block-encoding circuit can be created:

    def block_encode(A, control_wires):
        probs = A.coeffs / np.sum(A.coeffs)
        state = np.pad(np.sqrt(probs, dtype=complex), (0, 1))
        unitaries = A.ops
    
        qml.StatePrep(state, wires=control_wires)
        qml.Select(unitaries, control=control_wires)
        qml.adjoint(qml.StatePrep)(state, wires=control_wires)
    
    >>> print(qml.draw(block_encode, show_matrices=False)(A, control_wires=[0, 1]))
    0: ─╭|Ψ⟩─╭Select─╭|Ψ⟩†─┤
    1: ─╰|Ψ⟩─├Select─╰|Ψ⟩†─┤
    2: ──────╰Select───────┤
    

    This circuit can be used as a building block within a larger QNode to perform algorithms such as QSVT and Hamiltonian simulation.

  • Decomposing a Hermitian matrix into a linear combination of Pauli words via qml.pauli_decompose is now faster and differentiable. (#4395) (#4479) (#4493)

    def find_coeffs(p):
        mat = np.array([[3, p], [p, 3]])
        A = qml.pauli_decompose(mat)
        return A.coeffs
    
    >>> import jax
    >>> from jax import numpy as np
    >>> jax.jacobian(find_coeffs)(np.array(2.))
    Array([0., 1.], dtype=float32, weak_type=True)
    

Monitor PennyLane's inner workings with logging 📃

  • Python-native logging can now be enabled with qml.logging.enable_logging(). (#4377) (#4383)

    Consider the following code that is contained in my_code.py:

    import pennylane as qml
    qml.logging.enable_logging()  # enables logging
    
    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev)
    def f(x):
        qml.RX(x, wires=0)
        return qml.state()
    
    f(0.5)
    

    Executing my_code.py with logging enabled will detail every step in PennyLane’s pipeline that gets used to run your code.

    $ python my_code.py
    [1967-02-13 15:18:38,591][DEBUG][<PID 8881:MainProcess>] - pennylane.qnode.__init__()::"Creating QNode(func=<function f at 0x7faf2a6fbaf0>, device=<DefaultQubit device (wires=2, shots=None) at 0x7faf2a689b50>, interface=auto, diff_method=best, expansion_strategy=gradient, max_expansion=10, grad_on_execution=best, mode=None, cache=True, cachesize=10000, max_diff=1, gradient_kwargs={}"
    ...
    

    Additional logging configuration settings can be specified by modifying the contents of the logging configuration file, which can be located by running qml.logging.config_path(). Follow our logging docs page for more details!

More input states for quantum chemistry calculations ⚛️

  • Input states obtained from advanced quantum chemistry calculations can be used in a circuit. (#4427) (#4433) (#4461) (#4476) (#4505)

    Quantum chemistry calculations rely on an initial state that is typically selected to be the trivial Hartree-Fock state. For molecules with a complicated electronic structure, using initial states obtained from affordable post-Hartree-Fock calculations helps to improve the efficiency of the quantum simulations. These calculations can be done with external quantum chemistry libraries such as PySCF.

    It is now possible to import a PySCF solver object in PennyLane and extract the corresponding wave function in the form of a state vector that can be directly used in a circuit. First, perform your classical quantum chemistry calculations and then use the qml.qchem.import_state function to import the solver object and return a state vector.

   >>> from pyscf import gto, scf, ci
   >>> mol = gto.M(atom=[['H', (0, 0, 0)], ['H', (0,0,0.71)]], basis='sto6g')
   >>> myhf = scf.UHF(mol).run()
   >>> myci = ci.UCISD(myhf).run()
   >>> wf_cisd = qml.qchem.import_state(myci, tol=1e-1)
   >>> print(wf_cisd)
   [ 0.        +0.j  0.        +0.j  0.        +0.j  0.1066467 +0.j
     1.        +0.j  0.        +0.j  0.        +0.j  0.        +0.j
     2.        +0.j  0.        +0.j  0.        +0.j  0.        +0.j
    -0.99429698+0.j  0.        +0.j  0.        +0.j  0.        +0.j]

The state vector can be implemented in a circuit using ``qml.StatePrep``.
   >>> dev = qml.device('default.qubit', wires=4)
   >>> @qml.qnode(dev)
   ... def circuit():
   ...     qml.StatePrep(wf_cisd, wires=range(4))
   ...     return qml.state()
   >>> print(circuit())
   [ 0.        +0.j  0.        +0.j  0.        +0.j  0.1066467 +0.j
     1.        +0.j  0.        +0.j  0.        +0.j  0.        +0.j
     2.        +0.j  0.        +0.j  0.        +0.j  0.        +0.j
    -0.99429698+0.j  0.        +0.j  0.        +0.j  0.        +0.j]

The currently supported post-Hartree-Fock methods are RCISD, UCISD, RCCSD, and UCCSD which
denote restricted (R) and unrestricted (U) configuration interaction (CI) and coupled cluster (CC)
calculations with single and double (SD) excitations.

Reuse and reset qubits after mid-circuit measurements ♻️

  • PennyLane now allows you to define circuits that reuse a qubit after a mid-circuit measurement has taken place. Optionally, the wire can also be reset to the \(|0\rangle\) state. (#4402) (#4432)

    Post-measurement reset can be activated by setting reset=True when calling qml.measure. In this version of PennyLane, executing circuits with qubit reuse will result in the defer_measurements transform being applied. This transform replaces each reused wire with an additional qubit. However, future releases of PennyLane will explore device-level support for qubit reuse without consuming additional qubits.

    Qubit reuse and reset is also fully differentiable:

    dev = qml.device("default.qubit", wires=4)
    
    @qml.qnode(dev)
    def circuit(p):
        qml.RX(p, wires=0)
        m = qml.measure(0, reset=True)
        qml.cond(m, qml.Hadamard)(1)
    
        qml.RX(p, wires=0)
        m = qml.measure(0)
        qml.cond(m, qml.Hadamard)(1)
        return qml.expval(qml.PauliZ(1))
    
    >>> jax.grad(circuit)(0.4)
    Array(-0.35867804, dtype=float32, weak_type=True)
    

    You can read more about mid-circuit measurements in the documentation, and stay tuned for more mid-circuit measurement features in the next few releases!

Improvements 🛠

A new PennyLane drawing style

  • Circuit drawings and plots can now be created following a PennyLane style. (#3950)

    The qml.draw_mpl function accepts a style='pennylane' argument to create PennyLane themed circuit diagrams:

    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))
    
    qml.draw_mpl(circuit, style="pennylane")(1, 1)
    

    PennyLane-styled plots can also be drawn by passing "pennylane.drawer.plot" to Matplotlib’s plt.style.use function:

    import matplotlib.pyplot as plt
    
    plt.style.use("pennylane.drawer.plot")
    for i in range(3):
        plt.plot(np.random.rand(10))
    

    If the font Quicksand Bold isn’t available, an available default font is used instead.

Making operators immutable and PyTrees

  • Any class inheriting from Operator is now automatically registered as a pytree with JAX. This unlocks the ability to jit functions of Operator. (#4458)

    >>> op = qml.adjoint(qml.RX(1.0, wires=0))
    >>> jax.jit(qml.matrix)(op)
    Array([[0.87758255-0.j        , 0.        +0.47942555j],
         [0.        +0.47942555j, 0.87758255-0.j        ]],      dtype=complex64, weak_type=True)
    >>> jax.tree_util.tree_map(lambda x: x+1, op)
    Adjoint(RX(2.0, wires=[0]))
    
  • All Operator objects now define Operator._flatten and Operator._unflatten methods that separate trainable from untrainable components. These methods will be used in serialization and pytree registration. Custom operations may need an update to ensure compatibility with new PennyLane features. (#4483) (#4314)

  • The QuantumScript class now has a bind_new_parameters method that allows creation of new QuantumScript objects with the provided parameters. (#4345)

  • The qml.gradients module no longer mutates operators in-place for any gradient transforms. Instead, operators that need to be mutated are copied with new parameters. (#4220)

  • PennyLane no longer directly relies on Operator.__eq__. (#4398)

  • qml.equal no longer raises errors when operators or measurements of different types are compared. Instead, it returns False. (#4315)

Transforms

  • Transform programs are now integrated with the QNode. (#4404)

    def null_postprocessing(results: qml.typing.ResultBatch) -> qml.typing.Result:
        return results[0]
    
    @qml.transforms.core.transform
    def scale_shots(tape: qml.tape.QuantumTape, shot_scaling) -> (Tuple[qml.tape.QuantumTape], Callable):
        new_shots = tape.shots.total_shots * shot_scaling
        new_tape = qml.tape.QuantumScript(tape.operations, tape.measurements, shots=new_shots)
        return (new_tape, ), null_postprocessing
    
    dev = qml.devices.experimental.DefaultQubit2()
    
    @partial(scale_shots, shot_scaling=2)
    @qml.qnode(dev, interface=None)
    def circuit():
        return qml.sample(wires=0)
    
    >>> circuit(shots=1)
    array([False, False])
    
  • Transform Programs, qml.transforms.core.TransformProgram, can now be called on a batch of circuits and return a new batch of circuits and a single post processing function. (#4364)

  • TransformDispatcher now allows registration of custom QNode transforms. (#4466)

  • QNode transforms in qml.qinfo now support custom wire labels. #4331

  • qml.transforms.adjoint_metric_tensor now uses the simulation tools in qml.devices.qubit instead of private methods of qml.devices.DefaultQubit. (#4456)

  • Auxiliary wires and device wires are now treated the same way in qml.transforms.metric_tensor as in qml.gradients.hadamard_grad. All valid wire input formats for aux_wire are supported. (#4328)

Next-generation device API

  • The experimental device interface has been integrated with the QNode for JAX, JAX-JIT, TensorFlow and PyTorch. (#4323) (#4352) (#4392) (#4393)

  • The experimental DefaultQubit2 device now supports computing VJPs and JVPs using the adjoint method. (#4374)

  • New functions called adjoint_jvp and adjoint_vjp that compute the JVP and VJP of a tape using the adjoint method have been added to qml.devices.qubit.adjoint_jacobian (#4358)

  • DefaultQubit2 now accepts a max_workers argument which controls multiprocessing. A ProcessPoolExecutor executes tapes asynchronously using a pool of at most max_workers processes. If max_workers is None or not given, only the current process executes tapes. If you experience any issue, say using JAX, TensorFlow, Torch, try setting max_workers to None. (#4319) (#4425)

  • qml.devices.experimental.Device now accepts a shots keyword argument and has a shots property. This property is only used to set defaults for a workflow, and does not directly influence the number of shots used in executions or derivatives. (#4388)

  • expand_fn() for DefaultQubit2 has been updated to decompose StatePrep operations present in the middle of a circuit. (#4444)

  • If no seed is specified on initialization with DefaultQubit2, the local random number generator will be seeded from NumPy’s global random number generator. (#4394)

Improvements to machine learning library interfaces

  • pennylane/interfaces has been refactored. The execute_fn passed to the machine learning framework boundaries is now responsible for converting parameters to NumPy. The gradients module can now handle TensorFlow parameters, but gradient tapes now retain the original dtype instead of converting to float64. This may cause instability with finite-difference differentiation and float32 parameters. The machine learning boundary functions are now uncoupled from their legacy counterparts. (#4415)

  • qml.interfaces.set_shots now accepts a Shots object as well as int‘s and tuples of int‘s. (#4388)

  • Readability improvements and stylistic changes have been made to pennylane/interfaces/jax_jit_tuple.py (#4379)

Pulses

  • A HardwareHamiltonian can now be summed with int or float objects. A sequence of HardwareHamiltonians can now be summed via the builtin sum. (#4343)

  • qml.pulse.transmon_drive has been updated in accordance with 1904.06560. In particular, the functional form has been changed from \(\Omega(t)(\cos(\omega_d t + \phi) X - \sin(\omega_d t + \phi) Y)$ to $\Omega(t) \sin(\omega_d t + \phi) Y\). (#4418) (#4465) (#4478) (#4418)

Other improvements

  • The qchem module has been upgraded to use the fermionic operators of the fermi module. #4336 #4521

  • The calculation of Sum, Prod, SProd, PauliWord, and PauliSentence sparse matrices are orders of magnitude faster. (#4475) (#4272) (#4411)

  • A function called qml.math.fidelity_statevector that computes the fidelity between two state vectors has been added. (#4322)

  • qml.ctrl(qml.PauliX) returns a CNOT, Toffoli, or MultiControlledX operation instead of Controlled(PauliX). (#4339)

  • When given a callable, qml.ctrl now does its custom pre-processing on all queued operators from the callable. (#4370)

  • The qchem functions primitive_norm and contracted_norm have been modified to be compatible with higher versions of SciPy. The private function _fac2 for computing double factorials has also been added. #4321

  • tape_expand now uses Operator.decomposition instead of Operator.expand in order to make more performant choices. (#4355)

  • CI now runs tests with TensorFlow 2.13.0 (#4472)

  • All tests in CI and pre-commit hooks now enable linting. (#4335)

  • The default label for a StatePrepBase operator is now |Ψ⟩. (#4340)

  • Device.default_expand_fn() has been updated to decompose qml.StatePrep operations present in the middle of a provided circuit. (#4437)

  • QNode.construct has been updated to only apply the qml.defer_measurements transform if the device does not natively support mid-circuit measurements. (#4516)

  • The application of the qml.defer_measurements transform has been moved from QNode.construct to qml.Device.batch_transform to allow more fine-grain control over when defer_measurements should be used. (#4432)

  • The label for ParametrizedEvolution can display parameters with the requested format as set by the kwarg decimals. Array-like parameters are displayed in the same format as matrices and stored in the cache. (#4151)

Breaking changes 💔

  • Applying gradient transforms to broadcasted/batched tapes has been deactivated until it is consistently supported for QNodes as well. (#4480)

  • Gradient transforms no longer implicitly cast float32 parameters to float64. Finite difference differentiation with float32 parameters may no longer give accurate results. (#4415)

  • The do_queue keyword argument in qml.operation.Operator has been removed. Instead of setting do_queue=False, use the qml.QueuingManager.stop_recording() context. (#4317)

  • Operator.expand now uses the output of Operator.decomposition instead of what it queues. (#4355)

  • The gradients module no longer needs shot information passed to it explicitly, as the shots are on the tapes. (#4448)

  • qml.StatePrep has been renamed to qml.StatePrepBase and qml.QubitStateVector has been renamed to qml.StatePrep. qml.operation.StatePrep and qml.QubitStateVector are still accessible. (#4450)

  • Support for Python 3.8 has been dropped. (#4453)

  • MeasurementValue‘s signature has been updated to accept a list of MidMeasureMP‘s rather than a list of their IDs. (#4446)

  • The grouping_type and grouping_method keyword arguments have been removed from qchem.molecular_hamiltonian. (#4301)

  • zyz_decomposition and xyx_decomposition have been removed. Use one_qubit_decomposition instead. (#4301)

  • LieAlgebraOptimizer has been removed. Use RiemannianGradientOptimizer instead. (#4301)

  • Operation.base_name has been removed. (#4301)

  • QuantumScript.name has been removed. (#4301)

  • qml.math.reduced_dm has been removed. Use qml.math.reduce_dm or qml.math.reduce_statevector instead. (#4301)

  • The qml.specs dictionary no longer supports direct key access to certain keys. (#4301)

    Instead, these quantities can be accessed as fields of the new Resources object saved under specs_dict["resources"]:

    • num_operations is no longer supported, use specs_dict["resources"].num_gates

    • num_used_wires is no longer supported, use specs_dict["resources"].num_wires

    • gate_types is no longer supported, use specs_dict["resources"].gate_types

    • gate_sizes is no longer supported, use specs_dict["resources"].gate_sizes

    • depth is no longer supported, use specs_dict["resources"].depth

  • qml.math.purity, qml.math.vn_entropy, qml.math.mutual_info, qml.math.fidelity, qml.math.relative_entropy, and qml.math.max_entropy no longer support state vectors as input. (#4322)

  • The private QuantumScript._prep list has been removed, and prep operations now go into the _ops list. (#4485)

Deprecations 👋

  • qml.enable_return and qml.disable_return have been deprecated. Please avoid calling disable_return, as the old return system has been deprecated along with these switch functions. (#4316)

  • qml.qchem.jordan_wigner has been deprecated. Use qml.jordan_wigner instead. List input to define the fermionic operator has also been deprecated; the fermionic operators in the qml.fermi module should be used instead. (#4332)

  • The qml.RandomLayers.compute_decomposition keyword argument ratio_imprimitive will be changed to ratio_imprim to match the call signature of the operation. (#4314)

  • The CV observables qml.X and qml.P have been deprecated. Use qml.QuadX and qml.QuadP instead. (#4330)

  • The method tape.unwrap() and corresponding UnwrapTape and Unwrap classes have been deprecated. Use convert_to_numpy_parameters instead. (#4344)

  • The mode keyword argument in QNode has been deprecated, as it was only used in the old return system (which has also been deprecated). Please use grad_on_execution instead. (#4316)

  • The QuantumScript.set_parameters method and the QuantumScript.data setter have been deprecated. Please use QuantumScript.bind_new_parameters instead. (#4346)

  • The __eq__ and __hash__ dunder methods of Operator and MeasurementProcess will now raise warnings to reflect upcoming changes to operator and measurement process equality and hashing. (#4144) (#4454) (#4489) (#4498)

  • The sampler_seed argument of qml.gradients.spsa_grad has been deprecated, along with a bug fix of the seed-setting behaviour. Instead, the sampler_rng argument should be set, either to an integer value, which will be used to create a PRNG internally or to a NumPy pseudo-random number generator created via np.random.default_rng(seed). (4165)

Documentation 📝

  • The qml.pulse.transmon_interaction and qml.pulse.transmon_drive documentation has been updated. #4327

  • qml.ApproxTimeEvolution.compute_decomposition() now has a code example. (#4354)

  • The documentation for qml.devices.experimental.Device has been improved to clarify some aspects of its use. (#4391)

  • Input types and sources for operators in qml.import_operator are specified. (#4476)

Bug fixes 🐛

  • qml.Projector is pickle-able again. (#4452)

  • _copy_and_shift_params does not cast or convert integral types, just relying on + and *‘s casting rules in this case. (#4477)

  • Sparse matrix calculations of SProds containing a Tensor are now allowed. When using Tensor.sparse_matrix(), it is recommended to use the wire_order keyword argument over wires. (#4424)

  • op.adjoint has been replaced with qml.adjoint in QNSPSAOptimizer. (#4421)

  • jax.ad (deprecated) has been replaced by jax.interpreters.ad. (#4403)

  • metric_tensor stops accidentally catching errors that stem from flawed wires assignments in the original circuit, leading to recursion errors. (#4328)

  • A warning is now raised if control indicators are hidden when calling qml.draw_mpl (#4295)

  • qml.qinfo.purity now produces correct results with custom wire labels. (#4331)

  • default.qutrit now supports all qutrit operations used with qml.adjoint. (#4348)

  • The observable data of qml.GellMann now includes its index, allowing correct comparison between instances of qml.GellMann, as well as Hamiltonians and Tensors containing qml.GellMann. (#4366)

  • qml.transforms.merge_amplitude_embedding now works correctly when the AmplitudeEmbeddings have a batch dimension. (#4353)

  • The jordan_wigner function has been modified to work with Hamiltonians built with an active space. (#4372)

  • When a style option is not provided, qml.draw_mpl uses the current style set from qml.drawer.use_style instead of black_white. (#4357)

  • qml.devices.qubit.preprocess.validate_and_expand_adjoint no longer sets the trainable parameters of the expanded tape. (#4365)

  • qml.default_expand_fn now selectively expands operations or measurements allowing more operations to be executed in circuits when measuring non-qwc Hamiltonians. (#4401)

  • qml.ControlledQubitUnitary no longer reports has_decomposition as True when it does not really have a decomposition. (#4407)

  • qml.transforms.split_non_commuting now correctly works on tapes containing both expval and var measurements. (#4426)

  • Subtracting a Prod from another operator now works as expected. (#4441)

  • The sampler_seed argument of qml.gradients.spsa_grad has been changed to sampler_rng. One can either provide an integer, which will be used to create a PRNG internally. Previously, this lead to the same direction being sampled, when num_directions is greater than 1. Alternatively, one can provide a NumPy PRNG, which allows reproducibly calling spsa_grad without getting the same results every time. (4165) (4482)

  • qml.math.get_dtype_name now works with autograd array boxes. (#4494)

  • The backprop gradient of qml.math.fidelity is now correct. (#4380)

Contributors ✍️

This release contains contributions from (in alphabetical order):

Utkarsh Azad, Thomas Bromley, Isaac De Vlugt, Amintor Dusko, Stepan Fomichev, Lillian M. A. Frederiksen, Soran Jahangiri, Edward Jiang, Korbinian Kottmann, Ivana Kurečić, Christina Lee, Vincent Michaud-Rioux, Romain Moyard, Lee James O’Riordan, Mudit Pandey, Borja Requena, Matthew Silverman, Jay Soni, David Wierichs, Frederik Wilde.

orphan

Release 0.31.0

New features since last release

Seamlessly create and combine fermionic operators 🔬

  • Fermionic operators and arithmetic are now available. (#4191) (#4195) (#4200) (#4201) (#4209) (#4229) (#4253) (#4255) (#4262) (#4278)

    There are a couple of ways to create fermionic operators with this new feature:

    • qml.FermiC and qml.FermiA: the fermionic creation and annihilation operators, respectively. These operators are defined by passing the index of the orbital that the fermionic operator acts on. For instance, the operators a⁺(0) and a(3) are respectively constructed as

      >>> qml.FermiC(0)
      a⁺(0)
      >>> qml.FermiA(3)
      a(3)
      

      These operators can be composed with (*) and linearly combined with (+ and -) other Fermi operators to create arbitrary fermionic Hamiltonians. Multiplying several Fermi operators together creates an operator that we call a Fermi word:

      >>> word = qml.FermiC(0) * qml.FermiA(0) * qml.FermiC(3) * qml.FermiA(3)
      >>> word
      a⁺(0) a(0) a⁺(3) a(3)
      

      Fermi words can be linearly combined to create a fermionic operator that we call a Fermi sentence:

      >>> sentence = 1.2 * word - 0.345 * qml.FermiC(3) * qml.FermiA(3)
      >>> sentence
      1.2 * a⁺(0) a(0) a⁺(3) a(3)
      - 0.345 * a⁺(3) a(3)
      
    • via qml.fermi.from_string: create a fermionic operator that represents multiple creation and annihilation operators being multiplied by each other (a Fermi word).

      >>> qml.fermi.from_string('0+ 1- 0+ 1-')
      a⁺(0) a(1) a⁺(0) a(1)
      >>> qml.fermi.from_string('0^ 1 0^ 1')
      a⁺(0) a(1) a⁺(0) a(1)
      

      Fermi words created with from_string can also be linearly combined to create a Fermi sentence:

      >>> word1 = qml.fermi.from_string('0+ 0- 3+ 3-')
      >>> word2 = qml.fermi.from_string('3+ 3-')
      >>> sentence = 1.2 * word1 + 0.345 * word2
      >>> sentence
      1.2 * a⁺(0) a(0) a⁺(3) a(3)
      + 0.345 * a⁺(3) a(3)
      

    Additionally, any fermionic operator, be it a single fermionic creation/annihilation operator, a Fermi word, or a Fermi sentence, can be mapped to the qubit basis by using qml.jordan_wigner:

    >>> qml.jordan_wigner(sentence)
    ((0.4725+0j)*(Identity(wires=[0]))) + ((-0.4725+0j)*(PauliZ(wires=[3]))) + ((-0.3+0j)*(PauliZ(wires=[0]))) + ((0.3+0j)*(PauliZ(wires=[0]) @ PauliZ(wires=[3])))
    

    Learn how to create fermionic Hamiltonians describing some simple chemical systems by checking out our fermionic operators demo!

Workflow-level resource estimation 🧮

  • PennyLane’s Tracker now monitors the resource requirements of circuits being executed by the device. (#4045) (#4110)

    Suppose we have a workflow that involves executing circuits with different qubit numbers. We can obtain the resource requirements as a function of the number of qubits by executing the workflow with the Tracker context:

    dev = qml.device("default.qubit", wires=4)
    
    @qml.qnode(dev)
    def circuit(n_wires):
        for i in range(n_wires):
            qml.Hadamard(i)
        return qml.probs(range(n_wires))
    
    with qml.Tracker(dev) as tracker:
        for i in range(1, 5):
            circuit(i)
    

    The resource requirements of individual circuits can then be inspected as follows:

    >>> resources = tracker.history["resources"]
    >>> resources[0]
    wires: 1
    gates: 1
    depth: 1
    shots: Shots(total=None)
    gate_types:
    {'Hadamard': 1}
    gate_sizes:
    {1: 1}
    >>> [r.num_wires for r in resources]
    [1, 2, 3, 4]
    

    Moreover, it is possible to predict the resource requirements without evaluating circuits using the null.qubit device, which follows the standard execution pipeline but returns numeric zeros. Consider the following workflow that takes the gradient of a 50-qubit circuit:

    n_wires = 50
    dev = qml.device("null.qubit", wires=n_wires)
    
    weight_shape = qml.StronglyEntanglingLayers.shape(2, n_wires)
    weights = np.random.random(weight_shape, requires_grad=True)
    
    @qml.qnode(dev, diff_method="parameter-shift")
    def circuit(weights):
        qml.StronglyEntanglingLayers(weights, wires=range(n_wires))
        return qml.expval(qml.PauliZ(0))
    
    with qml.Tracker(dev) as tracker:
        qml.grad(circuit)(weights)
    

    The tracker can be inspected to extract resource requirements without requiring a 50-qubit circuit run:

    >>> tracker.totals
    {'executions': 451, 'batches': 2, 'batch_len': 451}
    >>> tracker.history["resources"][0]
    wires: 50
    gates: 200
    depth: 77
    shots: Shots(total=None)
    gate_types:
    {'Rot': 100, 'CNOT': 100}
    gate_sizes:
    {1: 100, 2: 100}
    
  • Custom operations can now be constructed that solely define resource requirements — an explicit decomposition or matrix representation is not needed. (#4033)

    PennyLane is now able to estimate the total resource requirements of circuits that include one or more of these operations, allowing you to estimate requirements for high-level algorithms composed of abstract subroutines.

    These operations can be defined by inheriting from ResourcesOperation and overriding the resources() method to return an appropriate Resources object:

    class CustomOp(qml.resource.ResourcesOperation):
        def resources(self):
            n = len(self.wires)
            r = qml.resource.Resources(
                num_wires=n,
                num_gates=n ** 2,
                depth=5,
            )
            return r
    
    >>> wires = [0, 1, 2]
    >>> c = CustomOp(wires)
    >>> c.resources()
    wires: 3
    gates: 9
    depth: 5
    shots: Shots(total=None)
    gate_types:
    {}
    gate_sizes:
    {}
    

    A quantum circuit that contains CustomOp can be created and inspected using qml.specs:

    dev = qml.device("default.qubit", wires=wires)
    
    @qml.qnode(dev)
    def circ():
        qml.PauliZ(wires=0)
        CustomOp(wires)
        return qml.state()
    
    >>> specs = qml.specs(circ)()
    >>> specs["resources"].depth
    6
    

Community contributions from UnitaryHack 🤝

  • ParametrizedHamiltonian now has an improved string representation. (#4176)

    >>> def f1(p, t): return p[0] * jnp.sin(p[1] * t)
    >>> def f2(p, t): return p * t
    >>> coeffs = [2., f1, f2]
    >>> observables =  [qml.PauliX(0), qml.PauliY(0), qml.PauliZ(0)]
    >>> qml.dot(coeffs, observables)
      (2.0*(PauliX(wires=[0])))
    + (f1(params_0, t)*(PauliY(wires=[0])))
    + (f2(params_1, t)*(PauliZ(wires=[0])))
    
  • The quantum information module now supports trace distance. (#4181)

    Two cases are enabled for calculating the trace distance:

    • A QNode transform via qml.qinfo.trace_distance:

      dev = qml.device('default.qubit', wires=2)
      
      @qml.qnode(dev)
      def circuit(param):
          qml.RY(param, wires=0)
          qml.CNOT(wires=[0, 1])
          return qml.state()
      
      >>> trace_distance_circuit = qml.qinfo.trace_distance(circuit, circuit, wires0=[0], wires1=[0])
      >>> x, y = np.array(0.4), np.array(0.6)
      >>> trace_distance_circuit((x,), (y,))
      0.047862689546603415
      
    • Flexible post-processing via qml.math.trace_distance:

      >>> rho = np.array([[0.3, 0], [0, 0.7]])
      >>> sigma = np.array([[0.5, 0], [0, 0.5]])
      >>> qml.math.trace_distance(rho, sigma)
      0.19999999999999998
      
  • It is now possible to prepare qutrit basis states with qml.QutritBasisState. (#4185)

    wires = range(2)
    dev = qml.device("default.qutrit", wires=wires)
    
    @qml.qnode(dev)
    def qutrit_circuit():
        qml.QutritBasisState([1, 1], wires=wires)
        qml.TAdd(wires=wires)
        return qml.probs(wires=1)
    
    >>> qutrit_circuit()
    array([0., 0., 1.])
    
  • A new transform called one_qubit_decomposition has been added to provide a unified interface for decompositions of a single-qubit unitary matrix into sequences of X, Y, and Z rotations. All decompositions simplify the rotations angles to be between 0 and 4 pi. (#4210) (#4246)

    >>> from pennylane.transforms import one_qubit_decomposition
    >>> U = np.array([[-0.28829348-0.78829734j,  0.30364367+0.45085995j],
    ...               [ 0.53396245-0.10177564j,  0.76279558-0.35024096j]])
    >>> one_qubit_decomposition(U, 0, "ZYZ")
    [RZ(tensor(12.32427531, requires_grad=True), wires=[0]),
     RY(tensor(1.14938178, requires_grad=True), wires=[0]),
     RZ(tensor(1.73305815, requires_grad=True), wires=[0])]
    >>> one_qubit_decomposition(U, 0, "XYX", return_global_phase=True)
    [RX(tensor(10.84535137, requires_grad=True), wires=[0]),
     RY(tensor(1.39749741, requires_grad=True), wires=[0]),
     RX(tensor(0.45246584, requires_grad=True), wires=[0]),
     (0.38469215914523336-0.9230449299422961j)*(Identity(wires=[0]))]
    
  • The has_unitary_generator attribute in qml.ops.qubit.attributes no longer contains operators with non-unitary generators. (#4183)

  • PennyLane Docker builds have been updated to include the latest plugins and interface versions. (#4178)

Extended support for differentiating pulses ⚛️

  • The stochastic parameter-shift gradient method can now be used with hardware-compatible Hamiltonians. (#4132) (#4215)

    This new feature generalizes the stochastic parameter-shift gradient transform for pulses (stoch_pulse_grad) to support Hermitian generating terms beyond just Pauli words in pulse Hamiltonians, which makes it hardware-compatible.

  • A new differentiation method called qml.gradients.pulse_generator is available, which combines classical processing with the parameter-shift rule for multivariate gates to differentiate pulse programs. Access it in your pulse programs by setting diff_method=qml.gradients.pulse_generator. (#4160)

  • qml.pulse.ParametrizedEvolution now uses batched compressed sparse row (BCSR) format. This allows for computing Jacobians of the unitary directly even when dense=False. (#4126)

    def U(params):
        H = jnp.polyval * qml.PauliZ(0) # time dependent Hamiltonian
        Um = qml.evolve(H, dense=False)(params, t=10.)
        return qml.matrix(Um)
    params = jnp.array([[0.5]], dtype=complex)
    jac = jax.jacobian(U, holomorphic=True)(params)
    

Broadcasting and other tweaks to Torch and Keras layers 🦾

  • The TorchLayer and KerasLayer integrations with torch.nn and Keras have been upgraded. Consider the following TorchLayer:

    n_qubits = 2
    dev = qml.device("default.qubit", wires=n_qubits)
    
    @qml.qnode(dev)
    def qnode(inputs, weights):
        qml.AngleEmbedding(inputs, wires=range(n_qubits))
        qml.BasicEntanglerLayers(weights, wires=range(n_qubits))
        return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_qubits)]
    
    n_layers = 6
    weight_shapes = {"weights": (n_layers, n_qubits)}
    qlayer = qml.qnn.TorchLayer(qnode, weight_shapes)
    

    The following features are now available:

    • Native support for parameter broadcasting. (#4131)

      >>> batch_size = 10
      >>> inputs = torch.rand((batch_size, n_qubits))
      >>> qlayer(inputs)
      >>> dev.num_executions == 1
      True
      
    • The ability to draw a TorchLayer and KerasLayer using qml.draw() and qml.draw_mpl(). (#4197)

      >>> print(qml.draw(qlayer, show_matrices=False)(inputs))
      0: ─╭AngleEmbedding(M0)─╭BasicEntanglerLayers(M1)─┤  <Z>
      1: ─╰AngleEmbedding(M0)─╰BasicEntanglerLayers(M1)─┤  <Z>
      
    • Support for KerasLayer model saving and clearer instructions on TorchLayer model saving. (#4149) (#4158)

      >>> torch.save(qlayer.state_dict(), "weights.pt")  # Saving
      >>> qlayer.load_state_dict(torch.load("weights.pt"))  # Loading
      >>> qlayer.eval()
      

      Hybrid models containing KerasLayer or TorchLayer objects can also be saved and loaded.

Improvements 🛠

A more flexible projector

  • qml.Projector now accepts a state vector representation, which enables the creation of projectors in any basis. (#4192)

    dev = qml.device("default.qubit", wires=2)
    @qml.qnode(dev)
    def circuit(state):
        return qml.expval(qml.Projector(state, wires=[0, 1]))
    zero_state = [0, 0]
    plusplus_state = np.array([1, 1, 1, 1]) / 2
    
    >>> circuit(zero_state)
    tensor(1., requires_grad=True)
    >>> circuit(plusplus_state)
    tensor(0.25, requires_grad=True)
    

Do more with qutrits

  • Three qutrit rotation operators have been added that are analogous to RX, RY, and RZ:

    • qml.TRX: an X rotation

    • qml.TRY: a Y rotation

    • qml.TRZ: a Z rotation

    (#2845) (#2846) (#2847)

  • Qutrit devices now support parameter-shift differentiation. (#2845)

The qchem module

  • qchem.molecular_hamiltonian(), qchem.qubit_observable(), qchem.import_operator(), and qchem.dipole_moment() now return an arithmetic operator if enable_new_opmath() is active. (#4138) (#4159) (#4189) (#4204)

  • Non-cubic lattice support for all electron resource estimation has been added. (3956)

  • The qchem.molecular_hamiltonian() function has been upgraded to support custom wires for constructing differentiable Hamiltonians. The zero imaginary component of the Hamiltonian coefficients have been removed. (#4050) (#4094)

  • Jordan-Wigner transforms that cache Pauli gate objects have been accelerated. (#4046)

  • An error is now raised by qchem.molecular_hamiltonian when the dhf method is used for an open-shell system. This duplicates a similar error in qchem.Molecule but makes it clear that the pyscf backend can be used for open-shell calculations. (#4058)

  • Updated various qubit tapering methods to support operator arithmetic. (#4252)

Next-generation device API

  • The new device interface has been integrated with qml.execute for autograd, backpropagation, and no differentiation. (#3903)

  • Support for adjoint differentiation has been added to the DefaultQubit2 device. (#4037)

  • A new function called measure_with_samples that returns a sample-based measurement result given a state has been added. (#4083) (#4093) (#4162) (#4254)

  • DefaultQubit2.preprocess now returns a new ExecutionConfig object with decisions for gradient_method, use_device_gradient, and grad_on_execution. (#4102)

  • Support for sample-based measurements has been added to the DefaultQubit2 device. (#4105) (#4114) (#4133) (#4172)

  • The DefaultQubit2 device now has a seed keyword argument. (#4120)

  • Added a dense keyword to ParametrizedEvolution that allows forcing dense or sparse matrices. (#4079) (#4095) (#4285)

  • Adds the Type variables pennylane.typing.Result and pennylane.typing.ResultBatch for type hinting the result of an execution. (#4018)

  • qml.devices.ExecutionConfig no longer has a shots property, as it is now on the QuantumScript.
    It now has a use_device_gradient property. ExecutionConfig.grad_on_execution = None indicates a request for "best", instead of a string. (#4102)

  • The new device interface for Jax has been integrated with qml.execute. (#4137)

  • The new device interface is now integrated with qml.execute for Tensorflow. (#4169)

  • The experimental device DefaultQubit2 now supports qml.Snapshot. (#4193)

  • The experimental device interface is integrated with the QNode. (#4196)

  • The new device interface in integrated with qml.execute for Torch. (#4257)

Handling shots

  • QuantumScript now has a shots property, allowing shots to be tied to executions instead of devices. (#4067) (#4103) (#4106) (#4112)

  • Several Python built-in functions are now properly defined for instances of the Shots class.

    • print: printing Shots instances is now human-readable

    • str: converting Shots instances to human-readable strings

    • ==: equating two different Shots instances

    • hash: obtaining the hash values of Shots instances

    (#4081) (#4082)

  • qml.devices.ExecutionConfig no longer has a shots property, as it is now on the QuantumScript. It now has a use_device_gradient property. ExecutionConfig.grad_on_execution = None indicates a request for "best" instead of a string. (#4102)

  • QuantumScript.shots has been integrated with QNodes so that shots are placed on the QuantumScript during QNode construction. (#4110)

  • The gradients module has been updated to use the new Shots object internally (#4152)

Operators

  • qml.prod now accepts a single quantum function input for creating new Prod operators. (#4011)

  • DiagonalQubitUnitary now decomposes into RZ, IsingZZ and MultiRZ gates instead of a QubitUnitary operation with a dense matrix. (#4035)

  • All objects being queued in an AnnotatedQueue are now wrapped so that AnnotatedQueue is not dependent on the has of any operators or measurement processes. (#4087)

  • A dense keyword to ParametrizedEvolution that allows forcing dense or sparse matrices has been added. (#4079) (#4095)

  • Added a new function qml.ops.functions.bind_new_parameters that creates a copy of an operator with new parameters without mutating the original operator. (#4113) (#4256)

  • qml.CY has been moved from qml.ops.qubit.non_parametric_ops to qml.ops.op_math.controlled_ops and now inherits from qml.ops.op_math.ControlledOp. (#4116)

  • qml.CZ now inherits from the ControlledOp class and supports exponentiation to arbitrary powers with pow, which is no longer limited to integers. It also supports sparse_matrix and decomposition representations. (#4117)

  • The construction of the Pauli representation for the Sum class is now faster. (#4142)

  • qml.drawer.drawable_layers.drawable_layers and qml.CircuitGraph have been updated to not rely on Operator equality or hash to work correctly. (#4143)

Other improvements

  • A transform dispatcher and program have been added. (#4109) (#4187)

  • Reduced density matrix functionality has been added via qml.math.reduce_dm and qml.math.reduce_statevector. Both functions have broadcasting support. (#4173)

  • The following functions in qml.qinfo now support parameter broadcasting:

    • reduced_dm

    • purity

    • vn_entropy

    • mutual_info

    • fidelity

    • relative_entropy

    • trace_distance

    (#4234)

  • The following functions in qml.math now support parameter broadcasting:

    • purity

    • vn_entropy

    • mutual_info

    • fidelity

    • relative_entropy

    • max_entropy

    • sqrt_matrix

    (#4186)

  • pulse.ParametrizedEvolution now raises an error if the number of input parameters does not match the number of parametrized coefficients in the ParametrizedHamiltonian that generates it. An exception is made for HardwareHamiltonians which are not checked. (#4216)

  • The default value for the show_matrices keyword argument in all drawing methods is now True. This allows for quick insights into broadcasted tapes, for example. (#3920)

  • Type variables for qml.typing.Result and qml.typing.ResultBatch have been added for type hinting the result of an execution. (#4108)

  • The Jax-JIT interface now uses symbolic zeros to determine trainable parameters. (4075)

  • A new function called pauli.pauli_word_prefactor() that extracts the prefactor for a given Pauli word has been added. (#4164)

  • Variable-length argument lists of functions and methods in some docstrings is now more clear. (#4242)

  • qml.drawer.drawable_layers.drawable_layers and qml.CircuitGraph have been updated to not rely on Operator equality or hash to work correctly. (#4143)

  • Drawing mid-circuit measurements connected by classical control signals to conditional operations is now possible. (#4228)

  • The autograd interface now submits all required tapes in a single batch on the backward pass. (#4245)

Breaking changes 💔

  • The default value for the show_matrices keyword argument in all drawing methods is now True. This allows for quick insights into broadcasted tapes, for example. (#3920)

  • DiagonalQubitUnitary now decomposes into RZ, IsingZZ, and MultiRZ gates rather than a QubitUnitary. (#4035)

  • Jax trainable parameters are now Tracer instead of JVPTracer. It is not always the right definition for the JIT interface, but we update them in the custom JVP using symbolic zeros. (4075)

  • The experimental Device interface qml.devices.experimental.Device now requires that the preprocess method also returns an ExecutionConfig object. This allows the device to choose what "best" means for various hyperparameters like gradient_method and grad_on_execution. (#4007) (#4102)

  • Gradient transforms with Jax no longer support argnum. Use argnums instead. (#4076)

  • qml.collections, qml.op_sum, and qml.utils.sparse_hamiltonian have been removed. (#4071)

  • The pennylane.transforms.qcut module now uses (op, id(op)) as nodes in directed multigraphs that are used within the circuit cutting workflow instead of op. This change removes the dependency of the module on the hash of operators. (#4227)

  • Operator.data now returns a tuple instead of a list. (#4222)

  • The pulse differentiation methods, pulse_generator and stoch_pulse_grad, now raise an error when they are applied to a QNode directly. Instead, use differentiation via a JAX entry point (jax.grad, jax.jacobian, …). (#4241)

Deprecations 👋

  • LieAlgebraOptimizer has been renamed to RiemannianGradientOptimizer. [(#4153)(https://github.com/PennyLaneAI/pennylane/pull/4153)]

  • Operation.base_name has been deprecated. Please use Operation.name or type(op).__name__ instead.

  • QuantumScript‘s name keyword argument and property have been deprecated. This also affects QuantumTape and OperationRecorder. (#4141)

  • The qml.grouping module has been removed. Its functionality has been reorganized in the qml.pauli module.

  • The public methods of DefaultQubit are pending changes to follow the new device API, as used in DefaultQubit2. Warnings have been added to the docstrings to reflect this. (#4145)

  • qml.math.reduced_dm has been deprecated. Please use qml.math.reduce_dm or qml.math.reduce_statevector instead. (#4173)

  • qml.math.purity, qml.math.vn_entropy, qml.math.mutual_info, qml.math.fidelity, qml.math.relative_entropy, and qml.math.max_entropy no longer support state vectors as input. Please call qml.math.dm_from_state_vector on the input before passing to any of these functions. (#4186)

  • The do_queue keyword argument in qml.operation.Operator has been deprecated. Instead of setting do_queue=False, use the qml.QueuingManager.stop_recording() context. (#4148)

  • zyz_decomposition and xyx_decomposition are now deprecated in favour of one_qubit_decomposition. (#4230)

Documentation 📝

  • The documentation is updated to construct QuantumTape upon initialization instead of with queuing. (#4243)

  • The docstring for qml.ops.op_math.Pow.__new__ is now complete and it has been updated along with qml.ops.op_math.Adjoint.__new__. (#4231)

  • The docstring for qml.grad now states that it should be used with the Autograd interface only. (#4202)

  • The description of mult in the qchem.Molecule docstring now correctly states the value of mult that is supported. (#4058)

Bug Fixes 🐛

  • Fixed adjoint jacobian results with grad_on_execution=False in the JAX-JIT interface. (4217)

  • Fixed the matrix of SProd when the coefficient is tensorflow and the target matrix is not complex128. (#4249)

  • Fixed a bug where stoch_pulse_grad would ignore prefactors of rescaled Pauli words in the generating terms of a pulse Hamiltonian. (4156)

  • Fixed a bug where the wire ordering of the wires argument to qml.density_matrix was not taken into account. (#4072)

  • A patch in interfaces/autograd.py that checks for the strawberryfields.gbs device has been removed. That device is pinned to PennyLane <= v0.29.0, so that patch is no longer necessary. (#4089)

  • qml.pauli.are_identical_pauli_words now treats all identities as equal. Identity terms on Hamiltonians with non-standard wire orders are no longer eliminated. (#4161)

  • qml.pauli_sentence() is now compatible with empty Hamiltonians qml.Hamiltonian([], []). (#4171)

  • Fixed a bug with Jax where executing multiple tapes with gradient_fn="device" would fail. (#4190)

  • A more meaningful error message is raised when broadcasting with adjoint differentiation on DefaultQubit. (#4203)

  • The has_unitary_generator attribute in qml.ops.qubit.attributes no longer contains operators with non-unitary generators. (#4183)

  • Fixed a bug where op = qml.qsvt() was incorrect up to a global phase when using convention="Wx"" and qml.matrix(op). (#4214)

  • Fixed a buggy calculation of the angle in xyx_decomposition that causes it to give an incorrect decomposition. An if conditional was intended to prevent divide by zero errors, but the division was by the sine of the argument. So, any multiple of $pi$ should trigger the conditional, but it was only checking if the argument was 0. Example: qml.Rot(2.3, 2.3, 2.3) (#4210)

  • Fixed bug that caused ShotAdaptiveOptimizer to truncate dimensions of parameter-distributed shots during optimization. (#4240)

  • Sum observables can now have trainable parameters. (#4251) (#4275)

Contributors ✍️

This release contains contributions from (in alphabetical order):

Venkatakrishnan AnushKrishna, Utkarsh Azad, Thomas Bromley, Isaac De Vlugt, Lillian M. A. Frederiksen, Emiliano Godinez Ramirez Nikhil Harle Soran Jahangiri, Edward Jiang, Korbinian Kottmann, Christina Lee, Vincent Michaud-Rioux, Romain Moyard, Tristan Nemoz, Mudit Pandey, Manul Patel, Borja Requena, Modjtaba Shokrian-Zini, Mainak Roy, Matthew Silverman, Jay Soni, Edward Thomas, David Wierichs, Frederik Wilde.

orphan

Release 0.30.0

New features since last release

Pulse programming on hardware ⚛️🔬

  • Support for loading time-dependent Hamiltonians that are compatible with quantum hardware has been added, making it possible to load a Hamiltonian that describes an ensemble of Rydberg atoms or a collection of transmon qubits. (#3749) (#3911) (#3930) (#3936) (#3966) (#3987) (#4021) (#4040)

    Rydberg atoms are the foundational unit for neutral atom quantum computing. A Rydberg-system Hamiltonian can be constructed from a drive termqml.pulse.rydberg_drive — and an interaction termqml.pulse.rydberg_interaction:

    from jax import numpy as jnp
    
    atom_coordinates = [[0, 0], [0, 4], [4, 0], [4, 4]]
    wires = [0, 1, 2, 3]
    
    amplitude = lambda p, t: p * jnp.sin(jnp.pi * t)
    phase = jnp.pi / 2
    detuning = 3 * jnp.pi / 4
    
    H_d = qml.pulse.rydberg_drive(amplitude, phase, detuning, wires)
    H_i = qml.pulse.rydberg_interaction(atom_coordinates, wires)
    H = H_d + H_i
    

    The time-dependent Hamiltonian H can be used in a PennyLane pulse-level differentiable circuit:

    dev = qml.device("default.qubit.jax", wires=wires)
    
    @qml.qnode(dev, interface="jax")
    def circuit(params):
        qml.evolve(H)(params, t=[0, 10])
        return qml.expval(qml.PauliZ(0))
    
    >>> params = jnp.array([2.4])
    >>> circuit(params)
    Array(0.6316659, dtype=float32)
    >>> import jax
    >>> jax.grad(circuit)(params)
    Array([1.3116529], dtype=float32)
    

    The qml.pulse page contains additional details. Check out our release blog post for a demonstration of how to perform the execution on actual hardware!

  • A pulse-level circuit can now be differentiated using a stochastic parameter-shift method. (#3780) (#3900) (#4000) (#4004)

    The new qml.gradient.stoch_pulse_grad differentiation method unlocks stochastic-parameter-shift differentiation for pulse-level circuits. The current version of this new method is restricted to Hamiltonians composed of parametrized Pauli words, but future updates to extend to parametrized Pauli sentences can allow this method to be compatible with hardware-based systems such as an ensemble of Rydberg atoms.

    This method can be activated by setting diff_method to qml.gradient.stoch_pulse_grad:

    >>> dev = qml.device("default.qubit.jax", wires=2)
    >>> sin = lambda p, t: jax.numpy.sin(p * t)
    >>> ZZ = qml.PauliZ(0) @ qml.PauliZ(1)
    >>> H = 0.5 * qml.PauliX(0) + qml.pulse.constant * ZZ + sin * qml.PauliX(1)
    >>> @qml.qnode(dev, interface="jax", diff_method=qml.gradients.stoch_pulse_grad)
    >>> def ansatz(params):
    ...     qml.evolve(H)(params, (0.2, 1.))
    ...     return qml.expval(qml.PauliY(1))
    >>> params = [jax.numpy.array(0.4), jax.numpy.array(1.3)]
    >>> jax.grad(ansatz)(params)
    [Array(0.16921353, dtype=float32, weak_type=True),
     Array(-0.2537478, dtype=float32, weak_type=True)]
    

Quantum singular value transformation 🐛➡️🦋

  • PennyLane now supports the quantum singular value transformation (QSVT), which describes how a quantum circuit can be constructed to apply a polynomial transformation to the singular values of an input matrix. (#3756) (#3757) (#3758) (#3905) (#3909) (#3926) (#4023)

    Consider a matrix A along with a vector angles that describes the target polynomial transformation. The qml.qsvt function creates a corresponding circuit:

    dev = qml.device("default.qubit", wires=2)
    
    A = np.array([[0.1, 0.2], [0.3, 0.4]])
    angles = np.array([0.1, 0.2, 0.3])
    
    @qml.qnode(dev)
    def example_circuit(A):
        qml.qsvt(A, angles, wires=[0, 1])
        return qml.expval(qml.PauliZ(wires=0))
    

    This circuit is composed of qml.BlockEncode and qml.PCPhase operations.

    >>> example_circuit(A)
    tensor(0.97777078, requires_grad=True)
    >>> print(example_circuit.qtape.expand(depth=1).draw(decimals=2))
    0: ─╭∏_ϕ(0.30)─╭BlockEncode(M0)─╭∏_ϕ(0.20)─╭BlockEncode(M0)†─╭∏_ϕ(0.10)─┤  <Z>
    1: ─╰∏_ϕ(0.30)─╰BlockEncode(M0)─╰∏_ϕ(0.20)─╰BlockEncode(M0)†─╰∏_ϕ(0.10)─┤
    

    The qml.qsvt function creates a circuit that is targeted at simulators due to the use of matrix-based operations. For advanced users, you can use the operation-based qml.QSVT template to perform the transformation with a custom choice of unitary and projector operations, which may be hardware compatible if a decomposition is provided.

    The QSVT is a complex but powerful transformation capable of generalizing important algorithms like amplitude amplification. Stay tuned for a demo in the coming few weeks to learn more!

Intuitive QNode returns ↩️

  • An updated QNode return system has been introduced. PennyLane QNodes now return exactly what you tell them to! 🎉 (#3957) (#3969) (#3946) (#3913) (#3914) (#3934)

    This was an experimental feature introduced in version 0.25 of PennyLane that was enabled via qml.enable_return(). Now, it’s the default return system. Let’s see how it works.

    Consider the following circuit:

    import pennylane as qml
    
    dev = qml.device("default.qubit", wires=1)
    
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x, wires=0)
        return qml.expval(qml.PauliZ(0)), qml.probs(0)
    

    In version 0.29 and earlier of PennyLane, circuit() would return a single length-3 array:

    >>> circuit(0.5)
    tensor([0.87758256, 0.93879128, 0.06120872], requires_grad=True)
    

    In versions 0.30 and above, circuit() returns a length-2 tuple containing the expectation value and probabilities separately:

    >>> circuit(0.5)
    (tensor(0.87758256, requires_grad=True),
     tensor([0.93879128, 0.06120872], requires_grad=True))
    

    You can find more details about this change, along with help and troubleshooting tips to solve any issues. If you still have questions, comments, or concerns, we encourage you to post on the PennyLane discussion forum.

A bunch of performance tweaks 🏃💨

  • Single-qubit operations that have multi-qubit control can now be decomposed more efficiently using fewer CNOT gates. (#3851)

    Three decompositions from arXiv:2302.06377 are provided and compare favourably to the already-available qml.ops.ctrl_decomp_zyz:

    wires = [0, 1, 2, 3, 4, 5]
    control_wires = wires[1:]
    
    @qml.qnode(qml.device('default.qubit', wires=6))
    def circuit():
        with qml.QueuingManager.stop_recording():
            # the decomposition does not un-queue the target
            target = qml.RX(np.pi/2, wires=0)
        qml.ops.ctrl_decomp_bisect(target, (1, 2, 3, 4, 5))
        return qml.state()
    
    print(qml.draw(circuit, expansion_strategy="device")())
    
    0: ──H─╭X──U(M0)─╭X──U(M0)†─╭X──U(M0)─╭X──U(M0)†──H─┤  State
    1: ────├●────────│──────────├●────────│─────────────┤  State
    2: ────├●────────│──────────├●────────│─────────────┤  State
    3: ────╰●────────│──────────╰●────────│─────────────┤  State
    4: ──────────────├●───────────────────├●────────────┤  State
    5: ──────────────╰●───────────────────╰●────────────┤  State
    
  • A new decomposition to qml.SingleExcitation has been added that halves the number of CNOTs required. (3976)

    >>> qml.SingleExcitation.compute_decomposition(1.23, wires=(0,1))
    [Adjoint(T(wires=[0])), Hadamard(wires=[0]), S(wires=[0]),
     Adjoint(T(wires=[1])), Adjoint(S(wires=[1])), Hadamard(wires=[1]),
     CNOT(wires=[1, 0]), RZ(-0.615, wires=[0]), RY(0.615, wires=[1]),
     CNOT(wires=[1, 0]), Adjoint(S(wires=[0])), Hadamard(wires=[0]),
     T(wires=[0]), Hadamard(wires=[1]), S(wires=[1]), T(wires=[1])]
    
  • The adjoint differentiation method can now be more efficient, avoiding the decomposition of operations that can be differentiated directly. Any operation that defines a generator() can be differentiated with the adjoint method. (#3874)

    For example, in version 0.29 the qml.CRY operation would be decomposed when calculating the adjoint-method gradient. Executing the code below shows that this decomposition no longer takes place in version 0.30 and qml.CRY is differentiated directly:

    import jax
    from jax import numpy as jnp
    
    def compute_decomposition(self, phi, wires):
        print("A decomposition has been performed!")
        decomp_ops = [
            qml.RY(phi / 2, wires=wires[1]),
            qml.CNOT(wires=wires),
            qml.RY(-phi / 2, wires=wires[1]),
            qml.CNOT(wires=wires),
        ]
        return decomp_ops
    
    qml.CRY.compute_decomposition = compute_decomposition
    
    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev, diff_method="adjoint")
    def circuit(phi):
        qml.Hadamard(wires=0)
        qml.CRY(phi, wires=[0, 1])
        return qml.expval(qml.PauliZ(1))
    
    phi = jnp.array(0.5)
    jax.grad(circuit)(phi)
    
  • Derivatives are computed more efficiently when using jax.jit with gradient transforms; the trainable parameters are now set correctly instead of every parameter having to be set as trainable. (#3697)

    In the circuit below, only the derivative with respect to parameter b is now calculated:

    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev, interface="jax-jit")
    def circuit(a, b):
        qml.RX(a, wires=0)
        qml.RY(b, wires=0)
        qml.CNOT(wires=[0, 1])
        return qml.expval(qml.PauliZ(0))
    
    a = jnp.array(0.4)
    b = jnp.array(0.5)
    
    jac = jax.jacobian(circuit, argnums=[1])
    jac_jit = jax.jit(jac)
    
    jac_jit(a, b)
    assert len(circuit.tape.trainable_params) == 1
    

Improvements 🛠

Next-generation device API

In this release and future releases, we will be making changes to our device API with the goal in mind to make developing plugins much easier for developers and unlock new device capabilities. Users shouldn’t yet feel any of these changes when using PennyLane, but here is what has changed this release:

  • Several functions in devices/qubit have been added or improved:

    • sample_state: returns a series of samples based on a given state vector and a number of shots. (#3720)

    • simulate: supports measuring expectation values of large observables such as qml.Hamiltonian, qml.SparseHamiltonian, and qml.Sum. (#3759)

    • apply_operation: supports broadcasting. (#3852)

    • adjoint_jacobian: supports adjoint differentiation in the new qubit state-vector device. (#3790)

  • qml.devices.qubit.preprocess now allows circuits with non-commuting observables. (#3857)

  • qml.devices.qubit.measure now computes the expectation values of Hamiltonian and Sum in a backpropagation-compatible way. (#3862)

Pulse programming

  • Here are the functions, classes, and more that were added or improved to facilitate simulating ensembles of Rydberg atoms: (#3749) (#3911) (#3930) (#3936) (#3966) (#3987) (#3889) (#4021)

    • HardwareHamiltonian: an internal class that contains additional information about pulses and settings.

    • rydberg_interaction: a user-facing function that returns a HardwareHamiltonian containing the Hamiltonian of the interaction of all the Rydberg atoms.

    • transmon_interaction: a user-facing function for constructing the Hamiltonian that describes the circuit QED interaction Hamiltonian of superconducting transmon systems.

    • drive: a user-facing function function that returns a ParametrizedHamiltonian (HardwareHamiltonian) containing the Hamiltonian of the interaction between a driving electro-magnetic field and a group of qubits.

    • rydberg_drive: a user-facing function that returns a ParametrizedHamiltonian (HardwareHamiltonian) containing the Hamiltonian of the interaction between a driving laser field and a group of Rydberg atoms.

    • max_distance: a keyword argument added to qml.pulse.rydberg_interaction to allow for the removal of negligible contributions from atoms beyond max_distance from each other.

  • ParametrizedEvolution now takes two new Boolean keyword arguments: return_intermediate and complementary. They allow computing intermediate time evolution matrices. (#3900)

    Activating return_intermediate will return intermediate time evolution steps, for example for the matrix of the Operation, or of a quantum circuit when used in a QNode. Activating complementary will make these intermediate steps be the remaining time evolution complementary to the output for complementary=False. See the docstring for details.

  • Hardware-compatible pulse sequence gradients with qml.gradient.stoch_pulse_grad can now be calculated faster using the new keyword argument use_broadcasting. Executing a ParametrizedEvolution that returns intermediate evolutions has increased performance using the state vector ODE solver, as well. (#4000) (#4004)

Intuitive QNode returns

  • The QNode keyword argument mode has been replaced by the boolean grad_on_execution. (#3969)

  • The "default.gaussian" device and parameter-shift CV both support the new return system, but only for single measurements. (#3946)

  • Keras and Torch NN modules are now compatible with the new return type system. (#3913) (#3914)

  • DefaultQutrit now supports the new return system. (#3934)

Performance improvements

  • The efficiency of tapering(), tapering_hf() and clifford() have been improved. (3942)

  • The peak memory requirements of tapering() and tapering_hf() have been improved when used for larger observables. (3977)

  • Pauli arithmetic has been updated to convert to a Hamiltonian more efficiently. (#3939)

  • Operator has a new Boolean attribute has_generator. It returns whether or not the Operator has a generator defined. has_generator is used in qml.operation.has_gen, which improves its performance and extends differentiation support. (#3875)

  • The performance of CompositeOp has been significantly improved now that it overrides determining whether it is being used with a batch of parameters (see Operator._check_batching). Hamiltonian also now overrides this, but it does nothing since it does not support batching. (#3915)

  • The performance of a Sum operator has been significantly improved now that is_hermitian checks that all coefficients are real if the operator has a pre-computed Pauli representation. (#3915)

  • The coefficients function and the visualize submodule of the qml.fourier module now allow assigning different degrees for different parameters of the input function. (#3005)

    Previously, the arguments degree and filter_threshold to qml.fourier.coefficients were expected to be integers. Now, they can be a sequences of integers with one integer per function parameter (i.e. len(degree)==n_inputs), resulting in a returned array with shape (2*degrees[0]+1,..., 2*degrees[-1]+1). The functions in qml.fourier.visualize accordingly accept such arrays of coefficients.

Other improvements

  • A Shots class has been added to the measurements module to hold shot-related data. (#3682)

  • The custom JVP rules in PennyLane also now support non-scalar and mixed-shape tape parameters as well as multi-dimensional tape return types, like broadcasted qml.probs, for example. (#3766)

  • The qchem.jordan_wigner function has been extended to support more fermionic operator orders. (#3754) (#3751)

  • The AdaptiveOptimizer has been updated to use non-default user-defined QNode arguments. (#3765)

  • Operators now use TensorLike types dunder methods. (#3749)

  • qml.QubitStateVector.state_vector now supports broadcasting. (#3852)

  • qml.SparseHamiltonian can now be applied to any wires in a circuit rather than being restricted to all wires in the circuit. (#3888)

  • Operators can now be divided by scalars with / with the addition of the Operation.__truediv__ dunder method. (#3749)

  • Printing an instance of MutualInfoMP now displays the distribution of the wires between the two subsystems. (#3898)

  • Operator.num_wires has been changed from an abstract value to AnyWires. (#3919)

  • qml.transforms.sum_expand is not run in Device.batch_transform if the device supports Sum observables. (#3915)

  • The type of n_electrons in qml.qchem.Molecule has been set to int. (#3885)

  • Explicit errors have been added to QutritDevice if classical_shadow or shadow_expval is measured. (#3934)

  • QubitDevice now defines the private _get_diagonalizing_gates(circuit) method and uses it when executing circuits. This allows devices that inherit from QubitDevice to override and customize their definition of diagonalizing gates. (#3938)

  • retworkx has been renamed to rustworkx to accommodate the change in the package name. (#3975)

  • Exp, Sum, Prod, and SProd operator data is now a flat list instead of nested. (#3958) (#3983)

  • qml.transforms.convert_to_numpy_parameters has been added to convert a circuit with interface-specific parameters to one with only numpy parameters. This transform is designed to replace qml.tape.Unwrap. (#3899)

  • qml.operation.WiresEnum.AllWires is now -2 instead of 0 to avoid the ambiguity between op.num_wires = 0 and op.num_wires = AllWires. (#3978)

  • Execution code has been updated to use the new qml.transforms.convert_to_numpy_parameters instead of qml.tape.Unwrap. (#3989)

  • A sub-routine of expand_tape has been converted into qml.tape.tape.rotations_and_diagonal_measurements, a helper function that computes rotations and diagonal measurements for a tape with measurements with overlapping wires. (#3912)

  • Various operators and templates have been updated to ensure that their decompositions only return lists of operators. (#3243)

  • The qml.operation.enable_new_opmath toggle has been introduced to cause dunder methods to return arithmetic operators instead of a Hamiltonian or Tensor. (#4008)

    >>> type(qml.PauliX(0) @ qml.PauliZ(1))
    <class 'pennylane.operation.Tensor'>
    >>> qml.operation.enable_new_opmath()
    >>> type(qml.PauliX(0) @ qml.PauliZ(1))
    <class 'pennylane.ops.op_math.prod.Prod'>
    >>> qml.operation.disable_new_opmath()
    >>> type(qml.PauliX(0) @ qml.PauliZ(1))
    <class 'pennylane.operation.Tensor'>
    
  • A new data class called Resources has been added to store resources like the number of gates and circuit depth throughout a quantum circuit. (#3981)

  • A new function called _count_resources() has been added to count the resources required when executing a QuantumTape for a given number of shots. (#3996)

  • QuantumScript.specs has been modified to make use of the new Resources class. This also modifies the output of qml.specs(). (#4015)

  • A new class called ResourcesOperation has been added to allow users to define operations with custom resource information. (#4026)

    For example, users can define a custom operation by inheriting from this new class:

    >>> class CustomOp(qml.resource.ResourcesOperation):
    ...     def resources(self):
    ...         return qml.resource.Resources(num_wires=1, num_gates=2,
    ...                                       gate_types={"PauliX": 2})
    ...
    >>> CustomOp(wires=1)
    CustomOp(wires=[1])
    

    Then, we can track and display the resources of the workflow using qml.specs():

    >>> dev = qml.device("default.qubit", wires=[0,1])
    >>> @qml.qnode(dev)
    ... def circ():
    ...     qml.PauliZ(wires=0)
    ...     CustomOp(wires=1)
    ...     return qml.state()
    ...
    >>> print(qml.specs(circ)()['resources'])
    wires: 2
    gates: 3
    depth: 1
    shots: 0
    gate_types:
    {'PauliZ': 1, 'PauliX': 2}
    
  • MeasurementProcess.shape now accepts a Shots object as one of its arguments to reduce exposure to unnecessary execution details. (#4012)

Breaking changes 💔

  • The seed_recipes argument has been removed from qml.classical_shadow and qml.shadow_expval. (#4020)

  • The tape method get_operation has an updated signature. (#3998)

  • Both JIT interfaces are no longer compatible with JAX >0.4.3 (we raise an error for those versions). (#3877)

  • An operation that implements a custom generator method, but does not always return a valid generator, also has to implement a has_generator property that reflects in which scenarios a generator will be returned. (#3875)

  • Trainable parameters for the Jax interface are the parameters that are JVPTracer, defined by setting argnums. Previously, all JAX tracers, including those used for JIT compilation, were interpreted to be trainable. (#3697)

  • The keyword argument argnums is now used for gradient transforms using Jax instead of argnum. argnum is automatically converted to argnums when using Jax and will no longer be supported in v0.31 of PennyLane. (#3697) (#3847)

  • qml.OrbitalRotation and, consequently, qml.GateFabric are now more consistent with the interleaved Jordan-Wigner ordering. Previously, they were consistent with the sequential Jordan-Wigner ordering. (#3861)

  • Some MeasurementProcess classes can now only be instantiated with arguments that they will actually use. For example, you can no longer create StateMP(qml.PauliX(0)) or PurityMP(eigvals=(-1,1), wires=Wires(0)). (#3898)

  • Exp, Sum, Prod, and SProd operator data is now a flat list, instead of nested. (#3958) (#3983)

  • qml.tape.tape.expand_tape and, consequentially, QuantumScript.expand no longer update the input tape with rotations and diagonal measurements. Note that the newly expanded tape that is returned will still have the rotations and diagonal measurements. (#3912)

  • qml.Evolution now initializes the coefficient with a factor of -1j instead of 1j. (#4024)

Deprecations 👋

Nothing for this release!

Documentation 📝

  • The documentation of QubitUnitary and DiagonalQubitUnitary was clarified regarding the parameters of the operations. (#4031)

  • A typo has been corrected in the documentation for the introduction to inspecting_circuits and chemistry. (#3844)

  • Usage Details and Theory sections have been separated in the documentation for qml.qchem.taper_operation. (3977)

Bug fixes 🐛

  • ctrl_decomp_bisect and ctrl_decomp_zyz are no longer used by default when decomposing controlled operations due to the presence of a global phase difference in the zyz decomposition of some target operators. (#4065)

  • Fixed a bug where qml.math.dot returned a numpy array instead of an autograd array, breaking autograd derivatives in certain circumstances. (#4019)

  • Operators now cast a tuple to an np.ndarray as well as list. (#4022)

  • Fixed a bug where qml.ctrl with parametric gates was incompatible with PyTorch tensors on GPUs. (#4002)

  • Fixed a bug where the broadcast expand results were stacked along the wrong axis for the new return system. (#3984)

  • A more informative error message is raised in qml.jacobian to explain potential problems with the new return types specification. (#3997)

  • Fixed a bug where calling Evolution.generator with coeff being a complex ArrayBox raised an error. (#3796)

  • MeasurementProcess.hash now uses the hash property of the observable. The property now depends on all properties that affect the behaviour of the object, such as VnEntropyMP.log_base or the distribution of wires between the two subsystems in MutualInfoMP. (#3898)

  • The enum measurements.Purity has been added so that PurityMP.return_type is defined. str and repr for PurityMP are also now defined. (#3898)

  • Sum.hash and Prod.hash have been changed slightly to work with non-numeric wire labels. sum_expand should now return correct results and not treat some products as the same operation. (#3898)

  • Fixed bug where the coefficients where not ordered correctly when summing a ParametrizedHamiltonian with other operators. (#3749) (#3902)

  • The metric tensor transform is now fully compatible with Jax and therefore users can provide multiple parameters. (#3847)

  • qml.math.ndim and qml.math.shape are now registered for built-ins and autograd to accomodate Autoray 0.6.1. #3864

  • Ensured that qml.data.load returns datasets in a stable and expected order. (#3856)

  • The qml.equal function now handles comparisons of ParametrizedEvolution operators. (#3870)

  • qml.devices.qubit.apply_operation catches the tf.errors.UnimplementedError that occurs when PauliZ or CNOT gates are applied to a large (>8 wires) tensorflow state. When that occurs, the logic falls back to the tensordot logic instead. (#3884)

  • Fixed parameter broadcasting support with qml.counts in most cases and introduced explicit errors otherwise. (#3876)

  • An error is now raised if a QNode with Jax-jit in use returns counts while having trainable parameters (#3892)

  • A correction has been added to the reference values in test_dipole_of to account for small changes (~2e-8) in the computed dipole moment values resulting from the new PySCF 2.2.0 release. (#3908)

  • SampleMP.shape is now correct when sampling only occurs on a subset of the device wires. (#3921)

  • An issue has been fixed in qchem.Molecule to allow basis sets other than the hard-coded ones to be used in the Molecule class. (#3955)

  • Fixed bug where all devices that inherit from DefaultQubit claimed to support ParametrizedEvolution. Now, only DefaultQubitJax supports the operator, as expected. (#3964)

  • Ensured that parallel AnnotatedQueues do not queue each other’s contents. (#3924)

  • Added a map_wires method to PauliWord and PauliSentence, and ensured that operators call it in their respective map_wires methods if they have a Pauli rep. (#3985)

  • Fixed a bug when a Tensor is multiplied by a Hamiltonian or vice versa. (#4036)

Contributors ✍️

This release contains contributions from (in alphabetical order):

Komi Amiko, Utkarsh Azad, Thomas Bromley, Isaac De Vlugt, Olivia Di Matteo, Lillian M. A. Frederiksen, Diego Guala, Soran Jahangiri, Korbinian Kottmann, Christina Lee, Vincent Michaud-Rioux, Albert Mitjans Coma, Romain Moyard, Lee J. O’Riordan, Mudit Pandey, Matthew Silverman, Jay Soni, David Wierichs.

orphan

Release 0.29.0

New features since last release

Pulse programming 🔊

  • Support for creating pulse-based circuits that describe evolution under a time-dependent Hamiltonian has now been added, as well as the ability to execute and differentiate these pulse-based circuits on simulator. (#3586) (#3617) (#3645) (#3652) (#3665) (#3673) (#3706) (#3730)

    A time-dependent Hamiltonian can be created using qml.pulse.ParametrizedHamiltonian, which holds information representing a linear combination of operators with parametrized coefficents and can be constructed as follows:

    from jax import numpy as jnp
    
    f1 = lambda p, t: p * jnp.sin(t) * (t - 1)
    f2 = lambda p, t: p[0] * jnp.cos(p[1]* t ** 2)
    
    XX = qml.PauliX(0) @ qml.PauliX(1)
    YY = qml.PauliY(0) @ qml.PauliY(1)
    ZZ = qml.PauliZ(0) @ qml.PauliZ(1)
    
    H =  2 * ZZ + f1 * XX + f2 * YY
    
    >>> H
    ParametrizedHamiltonian: terms=3
    >>> p1 = jnp.array(1.2)
    >>> p2 = jnp.array([2.3, 3.4])
    >>> H((p1, p2), t=0.5)
    (2*(PauliZ(wires=[0]) @ PauliZ(wires=[1]))) + ((-0.2876553231625218*(PauliX(wires=[0]) @ PauliX(wires=[1]))) + (1.517961235535459*(PauliY(wires=[0]) @ PauliY(wires=[1]))))
    

    The time-dependent Hamiltonian can be used within a circuit with qml.evolve:

    def pulse_circuit(params, time):
        qml.evolve(H)(params, time)
        return qml.expval(qml.PauliX(0) @ qml.PauliY(1))
    

    Pulse-based circuits can be executed and differentiated on the default.qubit.jax simulator using JAX as an interface:

    >>> dev = qml.device("default.qubit.jax", wires=2)
    >>> qnode = qml.QNode(pulse_circuit, dev, interface="jax")
    >>> params = (p1, p2)
    >>> qnode(params, time=0.5)
    Array(0.72153819, dtype=float64)
    >>> jax.grad(qnode)(params, time=0.5)
    (Array(-0.11324919, dtype=float64),
     Array([-0.64399616,  0.06326374], dtype=float64))
    

    Check out the qml.pulse documentation page for more details!

Special unitary operation 🌞

  • A new operation qml.SpecialUnitary has been added, providing access to an arbitrary unitary gate via a parametrization in the Pauli basis. (#3650) (#3651) (#3674)

    qml.SpecialUnitary creates a unitary that exponentiates a linear combination of all possible Pauli words in lexicographical order — except for the identity operator — for num_wires wires, of which there are 4**num_wires - 1. As its first argument, qml.SpecialUnitary takes a list of the 4**num_wires - 1 parameters that are the coefficients of the linear combination.

    To see all possible Pauli words for num_wires wires, you can use the qml.ops.qubit.special_unitary.pauli_basis_strings function:

    >>> qml.ops.qubit.special_unitary.pauli_basis_strings(1) # 4**1-1 = 3 Pauli words
    ['X', 'Y', 'Z']
    >>> qml.ops.qubit.special_unitary.pauli_basis_strings(2) # 4**2-1 = 15 Pauli words
    ['IX', 'IY', 'IZ', 'XI', 'XX', 'XY', 'XZ', 'YI', 'YX', 'YY', 'YZ', 'ZI', 'ZX', 'ZY', 'ZZ']
    

    To use qml.SpecialUnitary, for example, on a single qubit, we may define

    >>> thetas = np.array([0.2, 0.1, -0.5])
    >>> U = qml.SpecialUnitary(thetas, 0)
    >>> qml.matrix(U)
    array([[ 0.8537127 -0.47537233j,  0.09507447+0.19014893j],
           [-0.09507447+0.19014893j,  0.8537127 +0.47537233j]])
    

    A single non-zero entry in the parameters will create a Pauli rotation:

    >>> x = 0.412
    >>> theta = x * np.array([1, 0, 0]) # The first entry belongs to the Pauli word "X"
    >>> su = qml.SpecialUnitary(theta, wires=0)
    >>> rx = qml.RX(-2 * x, 0) # RX introduces a prefactor -0.5 that has to be compensated
    >>> qml.math.allclose(qml.matrix(su), qml.matrix(rx))
    True
    

    This operation can be differentiated with hardware-compatible methods like parameter shifts and it supports parameter broadcasting/batching, but not both at the same time. Learn more by visiting the qml.SpecialUnitary documentation.

Always differentiable 📈

  • The Hadamard test gradient transform is now available via qml.gradients.hadamard_grad. This transform is also available as a differentiation method within QNodes. (#3625) (#3736)

    qml.gradients.hadamard_grad is a hardware-compatible transform that calculates the gradient of a quantum circuit using the Hadamard test. Note that the device requires an auxiliary wire to calculate the gradient.

    >>> dev = qml.device("default.qubit", wires=2)
    >>> @qml.qnode(dev)
    ... def circuit(params):
    ...     qml.RX(params[0], wires=0)
    ...     qml.RY(params[1], wires=0)
    ...     qml.RX(params[2], wires=0)
    ...     return qml.expval(qml.PauliZ(0))
    >>> params = np.array([0.1, 0.2, 0.3], requires_grad=True)
    >>> qml.gradients.hadamard_grad(circuit)(params)
    (tensor(-0.3875172, requires_grad=True),
     tensor(-0.18884787, requires_grad=True),
     tensor(-0.38355704, requires_grad=True))
    

    This transform can be registered directly as the quantum gradient transform to use during autodifferentiation:

    >>> dev = qml.device("default.qubit", wires=2)
    >>> @qml.qnode(dev, interface="jax", diff_method="hadamard")
    ... def circuit(params):
    ...     qml.RX(params[0], wires=0)
    ...     qml.RY(params[1], wires=0)
    ...     qml.RX(params[2], wires=0)
    ...     return qml.expval(qml.PauliZ(0))
    >>> params = jax.numpy.array([0.1, 0.2, 0.3])
    >>> jax.jacobian(circuit)(params)
    Array([-0.3875172 , -0.18884787, -0.38355705], dtype=float32)
    
  • The gradient transform qml.gradients.spsa_grad is now registered as a differentiation method for QNodes. (#3440)

    The SPSA gradient transform can now be used implicitly by marking a QNode as differentiable with SPSA. It can be selected via

    >>> dev = qml.device("default.qubit", wires=1)
    >>> @qml.qnode(dev, interface="jax", diff_method="spsa", h=0.05, num_directions=20)
    ... def circuit(x):
    ...     qml.RX(x, 0)
    ...     return qml.expval(qml.PauliZ(0))
    >>> jax.jacobian(circuit)(jax.numpy.array(0.5))
    Array(-0.4792258, dtype=float32, weak_type=True)
    

    The argument num_directions determines how many directions of simultaneous perturbation are used and therefore the number of circuit evaluations, up to a prefactor. See the SPSA gradient transform documentation for details. Note: The full SPSA optimization method is already available as qml.SPSAOptimizer.

  • The default interface is now auto. There is no need to specify the interface anymore; it is automatically determined by checking your QNode parameters. (#3677) (#3752) (#3829)

    import jax
    import jax.numpy as jnp
    
    qml.enable_return()
    a = jnp.array(0.1)
    b = jnp.array(0.2)
    
    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev)
    def circuit(a, b):
        qml.RY(a, wires=0)
        qml.RX(b, wires=1)
        qml.CNOT(wires=[0, 1])
        return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1))
    
    >>> circuit(a, b)
    (Array(0.9950042, dtype=float32), Array(-0.19767681, dtype=float32))
    >>> jac = jax.jacobian(circuit)(a, b)
    >>> jac
    (Array(-0.09983341, dtype=float32, weak_type=True), Array(0.01983384, dtype=float32, weak_type=True))
    
  • The JAX-JIT interface now supports higher-order gradient computation with the new return types system. (#3498)

    Here is an example of using JAX-JIT to compute the Hessian of a circuit:

    import pennylane as qml
    import jax
    from jax import numpy as jnp
    
    jax.config.update("jax_enable_x64", True)
    
    qml.enable_return()
    
    dev = qml.device("default.qubit", wires=2)
    
    @jax.jit
    @qml.qnode(dev, interface="jax-jit", diff_method="parameter-shift", max_diff=2)
    def circuit(a, b):
        qml.RY(a, wires=0)
        qml.RX(b, wires=1)
        return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))
    
    a, b = jnp.array(1.0), jnp.array(2.0)
    
    >>> jax.hessian(circuit, argnums=[0, 1])(a, b)
    (((Array(-0.54030231, dtype=float64, weak_type=True),
       Array(0., dtype=float64, weak_type=True)),
      (Array(-1.76002563e-17, dtype=float64, weak_type=True),
       Array(0., dtype=float64, weak_type=True))),
     ((Array(0., dtype=float64, weak_type=True),
       Array(-1.00700085e-17, dtype=float64, weak_type=True)),
      (Array(0., dtype=float64, weak_type=True),
      Array(0.41614684, dtype=float64, weak_type=True))))
    
  • The qchem workflow has been modified to support both Autograd and JAX frameworks. (#3458) (#3462) (#3495)

    The JAX interface is automatically used when the differentiable parameters are JAX objects. Here is an example for computing the Hartree-Fock energy gradients with respect to the atomic coordinates.

    import pennylane as qml
    from pennylane import numpy as np
    import jax
    
    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 = [jax.numpy.array(mol.coordinates)]
    
    >>> jax.grad(qml.qchem.hf_energy(mol))(*args)
    Array([[ 0.       ,  0.       ,  0.3650435],
           [ 0.       ,  0.       , -0.3650435]], dtype=float64)
    
  • The kernel matrix utility functions in qml.kernels are now autodifferentiation-compatible. In addition, they support batching, for example for quantum kernel execution with shot vectors. (#3742)

    This allows for the following:

    dev = qml.device('default.qubit', wires=2, shots=(100, 100))
    @qml.qnode(dev)
    def circuit(x1, x2):
        qml.templates.AngleEmbedding(x1, wires=dev.wires)
        qml.adjoint(qml.templates.AngleEmbedding)(x2, wires=dev.wires)
        return qml.probs(wires=dev.wires)
    
    kernel = lambda x1, x2: circuit(x1, x2)
    

    We can then compute the kernel matrix on a set of 4 (random) feature vectors X but using two sets of 100 shots each via

    >>> X = np.random.random((4, 2))
    >>> qml.kernels.square_kernel_matrix(X, kernel)[:, 0]
    tensor([[[1.  , 0.86, 0.88, 0.92],
             [0.86, 1.  , 0.75, 0.97],
             [0.88, 0.75, 1.  , 0.91],
             [0.92, 0.97, 0.91, 1.  ]],
            [[1.  , 0.93, 0.91, 0.92],
             [0.93, 1.  , 0.8 , 1.  ],
             [0.91, 0.8 , 1.  , 0.91],
             [0.92, 1.  , 0.91, 1.  ]]], requires_grad=True)
    

    Note that we have extracted the first probability vector entry for each 100-shot evaluation.

Smartly decompose Hamiltonian evolution 💯

  • Hamiltonian evolution using qml.evolve or qml.exp can now be decomposed into operations. (#3691) (#3777)

    If the time-evolved Hamiltonian is equivalent to another PennyLane operation, then that operation is returned as the decomposition:

    >>> exp_op = qml.evolve(qml.PauliX(0) @ qml.PauliX(1))
    >>> exp_op.decomposition()
    [IsingXX((2+0j), wires=[0, 1])]
    

    If the Hamiltonian is a Pauli word, then the decomposition is provided as a qml.PauliRot operation:

    >>> qml.evolve(qml.PauliZ(0) @ qml.PauliX(1)).decomposition()
    [PauliRot((2+0j), ZX, wires=[0, 1])]
    

    Otherwise, the Hamiltonian is a linear combination of operators and the Suzuki-Trotter decomposition is used:

    >>> qml.evolve(qml.sum(qml.PauliX(0), qml.PauliY(0), qml.PauliZ(0)), num_steps=2).decomposition()
    [RX((1+0j), wires=[0]),
     RY((1+0j), wires=[0]),
     RZ((1+0j), wires=[0]),
     RX((1+0j), wires=[0]),
     RY((1+0j), wires=[0]),
     RZ((1+0j), wires=[0])]
    

Tools for quantum chemistry and other applications 🛠️

  • A new method called qml.qchem.givens_decomposition has been added, which decomposes a unitary into a sequence of Givens rotation gates with phase shifts and a diagonal phase matrix. (#3573)

    unitary = np.array([[ 0.73678+0.27511j, -0.5095 +0.10704j, -0.06847+0.32515j],
                        [-0.21271+0.34938j, -0.38853+0.36497j,  0.61467-0.41317j],
                        [ 0.41356-0.20765j, -0.00651-0.66689j,  0.32839-0.48293j]])
    
    phase_mat, ordered_rotations = qml.qchem.givens_decomposition(unitary)
    
    >>> phase_mat
    tensor([-0.20604358+0.9785369j , -0.82993272+0.55786114j,
            0.56230612-0.82692833j], requires_grad=True)
    >>> ordered_rotations
    [(tensor([[-0.65087861-0.63937521j, -0.40933651-0.j        ],
              [-0.29201359-0.28685265j,  0.91238348-0.j        ]], requires_grad=True),
      (0, 1)),
    (tensor([[ 0.47970366-0.33308926j, -0.8117487 -0.j        ],
              [ 0.66677093-0.46298215j,  0.5840069 -0.j        ]], requires_grad=True),
      (1, 2)),
    (tensor([[ 0.36147547+0.73779454j, -0.57008306-0.j        ],
              [ 0.2508207 +0.51194108j,  0.82158706-0.j        ]], requires_grad=True),
      (0, 1))]
    
  • A new template called qml.BasisRotation has been added, which performs a basis transformation defined by a set of fermionic ladder operators. (#3573)

    import pennylane as qml
    from pennylane import numpy as np
    
    V = np.array([[ 0.53672126+0.j        , -0.1126064 -2.41479668j],
                  [-0.1126064 +2.41479668j,  1.48694623+0.j        ]])
    eigen_vals, eigen_vecs = np.linalg.eigh(V)
    umat = eigen_vecs.T
    wires = range(len(umat))
    def circuit():
        qml.adjoint(qml.BasisRotation(wires=wires, unitary_matrix=umat))
        for idx, eigenval in enumerate(eigen_vals):
            qml.RZ(eigenval, wires=[idx])
        qml.BasisRotation(wires=wires, unitary_matrix=umat)
    
    >>> circ_unitary = qml.matrix(circuit)()
    >>> np.round(circ_unitary/circ_unitary[0][0], 3)
    tensor([[ 1.   -0.j   , -0.   +0.j   , -0.   +0.j   , -0.   +0.j   ],
            [-0.   +0.j   , -0.516-0.596j, -0.302-0.536j, -0.   +0.j   ],
            [-0.   +0.j   ,  0.35 +0.506j, -0.311-0.724j, -0.   +0.j   ],
            [-0.   +0.j   , -0.   +0.j   , -0.   +0.j   , -0.438+0.899j]], requires_grad=True)
    
  • A new function called qml.qchem.load_basisset has been added to extract qml.qchem basis set data from the Basis Set Exchange library. (#3363)

  • A new function called qml.math.max_entropy has been added to compute the maximum entropy of a quantum state. (#3594)

  • A new template called qml.TwoLocalSwapNetwork has been added that implements a canonical 2-complete linear (2-CCL) swap network described in arXiv:1905.05118. (#3447)

    dev = qml.device('default.qubit', wires=5)
    weights = np.random.random(size=qml.templates.TwoLocalSwapNetwork.shape(len(dev.wires)))
    acquaintances = lambda index, wires, param: (qml.CRY(param, wires=index)
                                     if np.abs(wires[0]-wires[1]) else qml.CRZ(param, wires=index))
    @qml.qnode(dev)
    def swap_network_circuit():
        qml.templates.TwoLocalSwapNetwork(dev.wires, acquaintances, weights, fermionic=False)
        return qml.state()
    
    >>> print(weights)
    tensor([0.20308242, 0.91906199, 0.67988804, 0.81290256, 0.08708985,
            0.81860084, 0.34448344, 0.05655892, 0.61781612, 0.51829044], requires_grad=True)
    >>> print(qml.draw(swap_network_circuit, expansion_strategy = 'device')())
    0: ─╭●────────╭SWAP─────────────────╭●────────╭SWAP─────────────────╭●────────╭SWAP─┤  State
    1: ─╰RY(0.20)─╰SWAP─╭●────────╭SWAP─╰RY(0.09)─╰SWAP─╭●────────╭SWAP─╰RY(0.62)─╰SWAP─┤  State
    2: ─╭●────────╭SWAP─╰RY(0.68)─╰SWAP─╭●────────╭SWAP─╰RY(0.34)─╰SWAP─╭●────────╭SWAP─┤  State
    3: ─╰RY(0.92)─╰SWAP─╭●────────╭SWAP─╰RY(0.82)─╰SWAP─╭●────────╭SWAP─╰RY(0.52)─╰SWAP─┤  State
    4: ─────────────────╰RY(0.81)─╰SWAP─────────────────╰RY(0.06)─╰SWAP─────────────────┤  State
    

Improvements 🛠

Pulse programming

  • A new function called qml.pulse.pwc has been added as a convenience function for defining a qml.pulse.ParametrizedHamiltonian. This function can be used to create a callable coefficient by setting the timespan over which the function should be non-zero. The resulting callable can be passed an array of parameters and a time. (#3645)

    >>> timespan = (2, 4)
    >>> f = qml.pulse.pwc(timespan)
    >>> f * qml.PauliX(0)
    ParametrizedHamiltonian: terms=1
    

    The params array will be used as bin values evenly distributed over the timespan, and the parameter t will determine which of the bins is returned.

    >>> f(params=[1.2, 2.3, 3.4, 4.5], t=3.9)
    DeviceArray(4.5, dtype=float32)
    >>> f(params=[1.2, 2.3, 3.4, 4.5], t=6)  # zero outside the range (2, 4)
    DeviceArray(0., dtype=float32)
    
  • A new function calledqml.pulse.pwc_from_function has been added as a decorator for defining a qml.pulse.ParametrizedHamiltonian. This function can be used to decorate a function and create a piecewise constant approximation of it. (#3645)

    >>> @qml.pulse.pwc_from_function((2, 4), num_bins=10)
    ... def f1(p, t):
    ...     return p * t
    

    The resulting function approximates the same of p**2 * t on the interval t=(2, 4) in 10 bins, and returns zero outside the interval.

    # t=2 and t=2.1 are within the same bin
    >>> f1(3, 2), f1(3, 2.1)
    (DeviceArray(6., dtype=float32), DeviceArray(6., dtype=float32))
    # next bin
    >>> f1(3, 2.2)
    DeviceArray(6.6666665, dtype=float32)
    # outside the interval t=(2, 4)
    >>> f1(3, 5)
    DeviceArray(0., dtype=float32)
    
  • Add ParametrizedHamiltonianPytree class, which is a pytree jax object representing a parametrized Hamiltonian, where the matrix computation is delayed to improve performance. (#3779)

Operations and batching

  • The function qml.dot has been updated to compute the dot product between a vector and a list of operators. (#3586)

    >>> coeffs = np.array([1.1, 2.2])
    >>> ops = [qml.PauliX(0), qml.PauliY(0)]
    >>> qml.dot(coeffs, ops)
    (1.1*(PauliX(wires=[0]))) + (2.2*(PauliY(wires=[0])))
    >>> qml.dot(coeffs, ops, pauli=True)
    1.1 * X(0) + 2.2 * Y(0)
    
  • qml.evolve returns the evolution of an Operator or a ParametrizedHamiltonian. (#3617) (#3706)

  • qml.ControlledQubitUnitary now inherits from qml.ops.op_math.ControlledOp, which defines decomposition, expand, and sparse_matrix rather than raising an error. (#3450)

  • Parameter broadcasting support has been added for the qml.ops.op_math.Controlled class if the base operator supports broadcasting. (#3450)

  • The qml.generator function now checks if the generator is Hermitian, rather than whether it is a subclass of Observable. This allows it to return valid generators from SymbolicOp and CompositeOp classes. (#3485)

  • The qml.equal function has been extended to compare Prod and Sum operators. (#3516)

  • qml.purity has been added as a measurement process for purity (#3551)

  • In-place inversion has been removed for qutrit operations in preparation for the removal of in-place inversion. (#3566)

  • The qml.utils.sparse_hamiltonian function has been moved to thee qml.Hamiltonian.sparse_matrix method. (#3585)

  • The qml.pauli.PauliSentence.operation() method has been improved to avoid instantiating an SProd operator when the coefficient is equal to 1. (#3595)

  • Batching is now allowed in all SymbolicOp operators, which include Exp, Pow and SProd. (#3597)

  • The Sum and Prod operations now have broadcasted operands. (#3611)

  • The XYX single-qubit unitary decomposition has been implemented. (#3628)

  • All dunder methods now return NotImplemented, allowing the right dunder method (e.g. __radd__) of the other class to be called. (#3631)

  • The qml.GellMann operators now include their index when displayed. (#3641)

  • qml.ops.ctrl_decomp_zyz has been added to compute the decomposition of a controlled single-qubit operation given a single-qubit operation and the control wires. (#3681)

  • qml.pauli.is_pauli_word now supports Prod and SProd operators, and it returns False when a Hamiltonian contains more than one term. (#3692)

  • qml.pauli.pauli_word_to_string now supports Prod, SProd and Hamiltonian operators. (#3692)

  • qml.ops.op_math.Controlled can now decompose single qubit target operations more effectively using the ZYZ decomposition. (#3726)

    • The qml.qchem.Molecule class raises an error when the molecule has an odd number of electrons or when the spin multiplicity is not 1. (#3748)

  • qml.qchem.basis_rotation now accounts for spin, allowing it to perform Basis Rotation Groupings for molecular hamiltonians. (#3714) (#3774)

  • The gradient transforms work for the new return type system with non-trivial classical jacobians. (#3776)

  • The default.mixed device has received a performance improvement for multi-qubit operations. This also allows to apply channels that act on more than seven qubits, which was not possible before. (#3584)

  • qml.dot now groups coefficients together. (#3691)

    >>> qml.dot(coeffs=[2, 2, 2], ops=[qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)])
    2*(PauliX(wires=[0]) + PauliY(wires=[1]) + PauliZ(wires=[2]))
    
  • qml.generator now supports operators with Sum and Prod generators. (#3691)

  • The Sum._sort method now takes into account the name of the operator when sorting. (#3691)

  • A new tape transform called qml.transforms.sign_expand has been added. It implements the optimal decomposition of a fast-forwardable Hamiltonian that minimizes the variance of its estimator in the Single-Qubit-Measurement from arXiv:2207.09479. (#2852)

Differentiability and interfaces

  • The qml.math module now also contains a submodule for fast Fourier transforms, qml.math.fft. (#1440)

    The submodule in particular provides differentiable versions of the following functions, available in all common interfaces for PennyLane

    Note that the output of the derivative of these functions may differ when used with complex-valued inputs, due to different conventions on complex-valued derivatives.

  • Validation has been added on gradient keyword arguments when initializing a QNode — if unexpected keyword arguments are passed, a UserWarning is raised. A list of the current expected gradient function keyword arguments can be accessed via qml.gradients.SUPPORTED_GRADIENT_KWARGS. (#3526)

  • The numpy version has been constrained to <1.24. (#3563)

  • Support for two-qubit unitary decomposition with JAX-JIT has been added. (#3569)

  • qml.math.size now supports PyTorch tensors. (#3606)

  • Most quantum channels are now fully differentiable on all interfaces. (#3612)

  • qml.math.matmul now supports PyTorch and Autograd tensors. (#3613)

  • Add qml.math.detach, which detaches a tensor from its trace. This stops automatic gradient computations. (#3674)

  • Add typing.TensorLike type. (#3675)

  • qml.QuantumMonteCarlo template is now JAX-JIT compatible when passing jax.numpy arrays to the template. (#3734)

  • DefaultQubitJax now supports evolving the state vector when executing qml.pulse.ParametrizedEvolution gates. (#3743)

  • SProd.sparse_matrix now supports interface-specific variables with a single element as the scalar. (#3770)

  • Added argnum argument to metric_tensor. By passing a sequence of indices referring to trainable tape parameters, the metric tensor is only computed with respect to these parameters. This reduces the number of tapes that have to be run. (#3587)

  • The parameter-shift derivative of variances saves a redundant evaluation of the corresponding unshifted expectation value tape, if possible (#3744)

Next generation device API

  • The apply_operation single-dispatch function is added to devices/qubit that applies an operation to a state and returns a new state. (#3637)

  • The preprocess function is added to devices/qubit that validates, expands, and transforms a batch of QuantumTape objects to abstract preprocessing details away from the device. (#3708)

  • The create_initial_state function is added to devices/qubit that returns an initial state for an execution. (#3683)

  • The simulate function is added to devices/qubit that turns a single quantum tape into a measurement result. The function only supports state based measurements with either no observables or observables with diagonalizing gates. It supports simultaneous measurement of non-commuting observables. (#3700)

  • The ExecutionConfig data class has been added. (#3649)

  • The StatePrep class has been added as an interface that state-prep operators must implement. (#3654)

  • qml.QubitStateVector now implements the StatePrep interface. (#3685)

  • qml.BasisState now implements the StatePrep interface. (#3693)

  • New Abstract Base Class for devices Device is added to the devices.experimental submodule. This interface is still in experimental mode and not integrated with the rest of pennylane. (#3602)

Other improvements

  • Writing Hamiltonians to a file using the qml.data module has been improved by employing a condensed writing format. (#3592)

  • Lazy-loading in the qml.data.Dataset.read() method is more universally supported. (#3605)

  • The qchem.Molecule class raises an error when the molecule has an odd number of electrons or when the spin multiplicity is not 1. (#3748)

  • qml.draw and qml.draw_mpl have been updated to draw any quantum function, which allows for visualizing only part of a complete circuit/QNode. (#3760)

  • The string representation of a Measurement Process now includes the _eigvals property if it is set. (#3820)

Breaking changes 💔

  • The argument mode in execution has been replaced by the boolean grad_on_execution in the new execution pipeline. (#3723)

  • qml.VQECost has been removed. (#3735)

  • The default interface is now auto. (#3677) (#3752) (#3829)

    The interface is determined during the QNode call instead of the initialization. It means that the gradient_fn and gradient_kwargs are only defined on the QNode at the beginning of the call. Moreover, without specifying the interface it is not possible to guarantee that the device will not be changed during the call if you are using backprop (such as default.qubit changing to default.qubit.jax) whereas before it was happening at initialization.

  • The tape method get_operation can also now return the operation index in the tape, and it can be activated by setting the return_op_index to True: get_operation(idx, return_op_index=True). It will become the default in version 0.30. (#3667)

  • Operation.inv() and the Operation.inverse setter have been removed. Please use qml.adjoint or qml.pow instead. (#3618)

    For example, instead of

    >>> qml.PauliX(0).inv()
    

    use

    >>> qml.adjoint(qml.PauliX(0))
    
  • The Operation.inverse property has been removed completely. (#3725)

  • The target wires of qml.ControlledQubitUnitary are no longer available via op.hyperparameters["u_wires"]. Instead, they can be accesses via op.base.wires or op.target_wires. (#3450)

  • The tape constructed by a QNode is no longer queued to surrounding contexts. (#3509)

  • Nested operators like Tensor, Hamiltonian, and Adjoint now remove their owned operators from the queue instead of updating their metadata to have an "owner". (#3282)

  • qml.qchem.scf, qml.RandomLayers.compute_decomposition, and qml.Wires.select_random now use local random number generators instead of global random number generators. This may lead to slightly different random numbers and an independence of the results from the global random number generation state. Please provide a seed to each individual function instead if you want controllable results. (#3624)

  • qml.transforms.measurement_grouping has been removed. Users should use qml.transforms.hamiltonian_expand instead. (#3701)

  • op.simplify() for operators which are linear combinations of Pauli words will use a builtin Pauli representation to more efficiently compute the simplification of the operator. (#3481)

  • All Operator‘s input parameters that are lists are cast into vanilla numpy arrays. (#3659)

  • QubitDevice.expval no longer permutes an observable’s wire order before passing it to QubitDevice.probability. The associated downstream changes for default.qubit have been made, but this may still affect expectations for other devices that inherit from QubitDevice and override probability (or any other helper functions that take a wire order such as marginal_prob, estimate_probability or analytic_probability). (#3753)

Deprecations 👋

  • qml.utils.sparse_hamiltonian function has been deprecated, and usage will now raise a warning. Instead, one should use the qml.Hamiltonian.sparse_matrix method. (#3585)

  • The collections module has been deprecated. (#3686) (#3687)

  • qml.op_sum has been deprecated. Users should use qml.sum instead. (#3686)

  • The use of Evolution directly has been deprecated. Users should use qml.evolve instead. This new function changes the sign of the given parameter. (#3706)

  • Use of qml.dot with a QNodeCollection has been deprecated. (#3586)

Documentation 📝

  • Revise note on GPU support in the circuit introduction. (#3836)

  • Make warning about vanilla version of NumPy for differentiation more prominent. (#3838)

  • The documentation for qml.operation has been improved. (#3664)

  • The code example in qml.SparseHamiltonian has been updated with the correct wire range. (#3643)

  • A hyperlink has been added in the text for a URL in the qml.qchem.mol_data docstring. (#3644)

  • A typo was corrected in the documentation for qml.math.vn_entropy. (#3740)

Bug fixes 🐛

  • Fixed a bug where measuring qml.probs in the computational basis with non-commuting measurements returned incorrect results. Now an error is raised. (#3811)

  • Fixed a bug where measuring qml.probs in the computational basis with non-commuting measurements returned incorrect results. Now an error is raised. (#3811)

  • Fixed a bug in the drawer where nested controlled operations would output the label of the operation being controlled, rather than the control values. (#3745)

  • Fixed a bug in qml.transforms.metric_tensor where prefactors of operation generators were taken into account multiple times, leading to wrong outputs for non-standard operations. (#3579)

  • Local random number generators are now used where possible to avoid mutating the global random state. (#3624)

  • The networkx version change being broken has been fixed by selectively skipping a qcut TensorFlow-JIT test. (#3609) (#3619)

  • Fixed the wires for the Y decomposition in the ZX calculus transform. (#3598)

  • qml.pauli.PauliWord is now pickle-able. (#3588)

  • Child classes of QuantumScript now return their own type when using SomeChildClass.from_queue. (#3501)

  • A typo has been fixed in the calculation and error messages in operation.py (#3536)

  • qml.data.Dataset.write() now ensures that any lazy-loaded values are loaded before they are written to a file. (#3605)

  • Tensor._batch_size is now set to None during initialization, copying and map_wires. (#3642) (#3661)

  • Tensor.has_matrix is now set to True. (#3647)

  • Fixed typo in the example of qml.IsingZZ gate decomposition. (#3676)

  • Fixed a bug that made tapes/qnodes using qml.Snapshot incompatible with qml.drawer.tape_mpl. (#3704)

  • Tensor._pauli_rep is set to None during initialization and Tensor.data has been added to its setter. (#3722)

  • qml.math.ndim has been redirected to jnp.ndim when using it on a jax tensor. (#3730)

  • Implementations of marginal_prob (and subsequently, qml.probs) now return probabilities with the expected wire order. (#3753)

    This bug affected most probabilistic measurement processes on devices that inherit from QubitDevice when the measured wires are out of order with respect to the device wires and 3 or more wires are measured. The assumption was that marginal probabilities would be computed with the device’s state and wire order, then re-ordered according to the measurement process wire order. Instead, the re-ordering went in the inverse direction (that is, from measurement process wire order to device wire order). This is now fixed. Note that this only occurred for 3 or more measured wires because this mapping is identical otherwise. More details and discussion of this bug can be found in the original bug report.

  • Empty iterables can no longer be returned from QNodes. (#3769)

  • The keyword arguments for qml.equal now are used when comparing the observables of a Measurement Process. The eigvals of measurements are only requested if both observables are None, saving computational effort. (#3820)

  • Only converts input to qml.Hermitian to a numpy array if the input is a list. (#3820)

Contributors ✍

This release contains contributions from (in alphabetical order):

Gian-Luca Anselmetti, Guillermo Alonso-Linaje, Juan Miguel Arrazola, Ikko Ashimine, Utkarsh Azad, Miriam Beddig, Cristian Boghiu, Thomas Bromley, Astral Cai, Isaac De Vlugt, Olivia Di Matteo, Lillian M. A. Frederiksen, Soran Jahangiri, Korbinian Kottmann, Christina Lee, Albert Mitjans Coma, Romain Moyard, Mudit Pandey, Borja Requena, Matthew Silverman, Jay Soni, Antal Száva, Frederik Wilde, David Wierichs, Moritz Willmann.

orphan

Release 0.28.0

New features since last release

Custom measurement processes 📐

  • Custom measurements can now be facilitated with the addition of the qml.measurements module. (#3286) (#3343) (#3288) (#3312) (#3287) (#3292) (#3287) (#3326) (#3327) (#3388) (#3439) (#3466)

    Within qml.measurements are new subclasses that allow for the possibility to create custom measurements:

    • SampleMeasurement: represents a sample-based measurement

    • StateMeasurement: represents a state-based measurement

    • MeasurementTransform: represents a measurement process that requires the application of a batch transform

    Creating a custom measurement involves making a class that inherits from one of the classes above. An example is given below. Here, the measurement computes the number of samples obtained of a given state:

    from pennylane.measurements import SampleMeasurement
    
    class CountState(SampleMeasurement):
        def __init__(self, state: str):
            self.state = state  # string identifying the state, e.g. "0101"
            wires = list(range(len(state)))
            super().__init__(wires=wires)
    
        def process_samples(self, samples, wire_order, shot_range, bin_size):
            counts_mp = qml.counts(wires=self._wires)
            counts = counts_mp.process_samples(samples, wire_order, shot_range, bin_size)
            return counts.get(self.state, 0)
    
        def __copy__(self):
            return CountState(state=self.state)
    

    We can now execute the new measurement in a QNode as follows.

    dev = qml.device("default.qubit", wires=1, shots=10000)
    
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x, wires=0)
        return CountState(state="1")
    
    >>> circuit(1.23)
    tensor(3303., requires_grad=True)
    

    Differentiability is also supported for this new measurement process:

    >>> x = qml.numpy.array(1.23, requires_grad=True)
    >>> qml.grad(circuit)(x)
    4715.000000000001
    

    For more information about these new features, see the documentation for ``qml.measurements` <https://docs.pennylane.ai/en/stable/code/qml_measurements.html>`_.

ZX Calculus 🧮

  • QNodes can now be converted into ZX diagrams via the PyZX framework. (#3446)

    ZX diagrams are the medium for which we can envision a quantum circuit as a graph in the ZX-calculus language, showing properties of quantum protocols in a visually compact and logically complete fashion.

    QNodes decorated with @qml.transforms.to_zx will return a PyZX graph that represents the computation in the ZX-calculus language.

    dev = qml.device("default.qubit", wires=2)
    
    @qml.transforms.to_zx
    @qml.qnode(device=dev)
    def circuit(p):
        qml.RZ(p[0], wires=1),
        qml.RZ(p[1], wires=1),
        qml.RX(p[2], wires=0),
        qml.PauliZ(wires=0),
        qml.RZ(p[3], wires=1),
        qml.PauliX(wires=1),
        qml.CNOT(wires=[0, 1]),
        qml.CNOT(wires=[1, 0]),
        qml.SWAP(wires=[0, 1]),
        return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
    
    >>> params = [5 / 4 * np.pi, 3 / 4 * np.pi, 0.1, 0.3]
    >>> circuit(params)
    Graph(20 vertices, 23 edges)
    

    Information about PyZX graphs can be found in the PyZX Graphs API.

QChem databases and basis sets ⚛️

  • The symbols and geometry of a compound from the PubChem database can now be accessed via qchem.mol_data(). (#3289) (#3378)

    >>> import pennylane as qml
    >>> from pennylane.qchem import mol_data
    >>> mol_data("BeH2")
    (['Be', 'H', 'H'],
     tensor([[ 4.79404621,  0.29290755,  0.        ],
                  [ 3.77945225, -0.29290755,  0.        ],
                  [ 5.80882913, -0.29290755,  0.        ]], requires_grad=True))
    >>> mol_data(223, "CID")
    (['N', 'H', 'H', 'H', 'H'],
     tensor([[ 0.        ,  0.        ,  0.        ],
                  [ 1.82264085,  0.52836742,  0.40402345],
                  [ 0.01417295, -1.67429735, -0.98038991],
                  [-0.98927163, -0.22714508,  1.65369933],
                  [-0.84773114,  1.373075  , -1.07733286]], requires_grad=True))
    
  • Perform quantum chemistry calculations with two new basis sets: 6-311g and CC-PVDZ. (#3279)

    >>> symbols = ["H", "He"]
    >>> geometry = np.array([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0]], requires_grad=False)
    >>> charge = 1
    >>> basis_names = ["6-311G", "CC-PVDZ"]
    >>> for basis_name in basis_names:
    ...     mol = qml.qchem.Molecule(symbols, geometry, charge=charge, basis_name=basis_name)
    ...     print(qml.qchem.hf_energy(mol)())
    [-2.84429531]
    [-2.84061284]
    

A bunch of new operators 👀

  • The controlled CZ gate and controlled Hadamard gate are now available via qml.CCZ and qml.CH, respectively. (#3408)

    >>> ccz = qml.CCZ(wires=[0, 1, 2])
    >>> qml.matrix(ccz)
    [[ 1  0  0  0  0  0  0  0]
     [ 0  1  0  0  0  0  0  0]
     [ 0  0  1  0  0  0  0  0]
     [ 0  0  0  1  0  0  0  0]
     [ 0  0  0  0  1  0  0  0]
     [ 0  0  0  0  0  1  0  0]
     [ 0  0  0  0  0  0  1  0]
     [ 0  0  0  0  0  0  0 -1]]
    >>> ch = qml.CH(wires=[0, 1])
    >>> qml.matrix(ch)
    [[ 1.          0.          0.          0.        ]
     [ 0.          1.          0.          0.        ]
     [ 0.          0.          0.70710678  0.70710678]
     [ 0.          0.          0.70710678 -0.70710678]]
    
  • Three new parametric operators, qml.CPhaseShift00, qml.CPhaseShift01, and qml.CPhaseShift10, are now available. Each of these operators performs a phase shift akin to qml.ControlledPhaseShift but on different positions of the state vector. (#2715)

    >>> dev = qml.device("default.qubit", wires=2)
    >>> @qml.qnode(dev)
    >>> def circuit():
    ...     qml.PauliX(wires=1)
    ...     qml.CPhaseShift01(phi=1.23, wires=[0,1])
    ...     return qml.state()
    ...
    >>> circuit()
    tensor([0.        +0.j       , 0.33423773+0.9424888j,
            1.        +0.j       , 0.        +0.j       ], requires_grad=True)
    
  • A new gate operation called qml.FermionicSWAP has been added. This implements the exchange of spin orbitals representing fermionic-modes while maintaining proper anti-symmetrization. (#3380)

    dev = qml.device('default.qubit', wires=2)
    
    @qml.qnode(dev)
    def circuit(phi):
        qml.BasisState(np.array([0, 1]), wires=[0, 1])
        qml.FermionicSWAP(phi, wires=[0, 1])
        return qml.state()
    
    >>> circuit(0.1)
    tensor([0.        +0.j        , 0.99750208+0.04991671j,
          0.00249792-0.04991671j, 0.        +0.j        ], requires_grad=True)
    
  • Create operators defined from a generator via qml.ops.op_math.Evolution. (#3375)

    qml.ops.op_math.Evolution defines the exponential of an operator $hat{O}$ of the form $e^{ixhat{O}}$, with a single trainable parameter, $x$. Limiting to a single trainable parameter allows the use of qml.gradients.param_shift to find the gradient with respect to the parameter $x$.

    dev = qml.device('default.qubit', wires=2)
    
    @qml.qnode(dev, diff_method=qml.gradients.param_shift)
    def circuit(phi):
        qml.ops.op_math.Evolution(qml.PauliX(0), -.5 * phi)
        return qml.expval(qml.PauliZ(0))
    
    >>> phi = np.array(1.2)
    >>> circuit(phi)
    tensor(0.36235775, requires_grad=True)
    >>> qml.grad(circuit)(phi)
    -0.9320390495504149
    
  • The qutrit Hadamard gate, qml.THadamard, is now available. (#3340)

    The operation accepts a subspace keyword argument which determines which variant of the qutrit Hadamard to use.

    >>> th = qml.THadamard(wires=0, subspace=[0, 1])
    >>> qml.matrix(th)
    array([[ 0.70710678+0.j,  0.70710678+0.j,  0.        +0.j],
          [ 0.70710678+0.j, -0.70710678+0.j,  0.        +0.j],
          [ 0.        +0.j,  0.        +0.j,  1.        +0.j]])
    

New transforms, functions, and more 😯

  • Calculating the purity of arbitrary quantum states is now supported. (#3290)

    The purity can be calculated in an analogous fashion to, say, the Von Neumann entropy:

    • qml.math.purity can be used as an in-line function:

      >>> x = [1, 0, 0, 1] / np.sqrt(2)
      >>> qml.math.purity(x, [0, 1])
      1.0
      >>> qml.math.purity(x, [0])
      0.5
      
      >>> x = [[1 / 2, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 1 / 2]]
      >>> qml.math.purity(x, [0, 1])
      0.5
      
    • qml.qinfo.transforms.purity can transform a QNode returning a state to a function that returns the purity:

      dev = qml.device("default.mixed", wires=2)
      
      @qml.qnode(dev)
      def circuit(x):
          qml.IsingXX(x, wires=[0, 1])
          return qml.state()
      
      >>> qml.qinfo.transforms.purity(circuit, wires=[0])(np.pi / 2)
      0.5
      >>> qml.qinfo.transforms.purity(circuit, wires=[0, 1])(np.pi / 2)
      1.0
      

    As with the other methods in qml.qinfo, the purity is fully differentiable:

    >>> param = np.array(np.pi / 4, requires_grad=True)
    >>> qml.grad(qml.qinfo.transforms.purity(circuit, wires=[0]))(param)
    -0.5
    
  • A new gradient transform, qml.gradients.spsa_grad, that is based on the idea of SPSA is now available. (#3366)

    This new transform allows users to compute a single estimate of a quantum gradient using simultaneous perturbation of parameters and a stochastic approximation. A QNode that takes, say, an argument x, the approximate gradient can be computed as follows.

    >>> dev = qml.device("default.qubit", wires=2)
    >>> x = np.array(0.4, requires_grad=True)
    >>> @qml.qnode(dev)
    ... def circuit(x):
    ...     qml.RX(x, 0)
    ...     qml.RX(x, 1)
    ...     return qml.expval(qml.PauliZ(0))
    >>> grad_fn = qml.gradients.spsa_grad(circuit, h=0.1, num_directions=1)
    >>> grad_fn(x)
    array(-0.38876964)
    

    The argument num_directions determines how many directions of simultaneous perturbation are used, which is proportional to the number of circuit evaluations. See the SPSA gradient transform documentation for details. Note that the full SPSA optimizer is already available as qml.SPSAOptimizer.

  • Multiple mid-circuit measurements can now be combined arithmetically to create new conditionals. (#3159)

    dev = qml.device("default.qubit", wires=3)
    
    @qml.qnode(dev)
    def circuit():
        qml.Hadamard(wires=0)
        qml.Hadamard(wires=1)
        m0 = qml.measure(wires=0)
        m1 = qml.measure(wires=1)
        combined = 2 * m1 + m0
        qml.cond(combined == 2, qml.RX)(1.3, wires=2)
        return qml.probs(wires=2)
    
    >>> circuit()
    [0.90843735 0.09156265]
    
  • A new method called pauli_decompose() has been added to the qml.pauli module, which takes a hermitian matrix, decomposes it in the Pauli basis, and returns it either as a qml.Hamiltonian or qml.PauliSentence instance. (#3384)

  • Operation or Hamiltonian instances can now be generated from a qml.PauliSentence or qml.PauliWord via the new operation() and hamiltonian() methods. (#3391)

  • A sum_expand function has been added for tapes, which splits a tape measuring a Sum expectation into mutliple tapes of summand expectations, and provides a function to recombine the results. (#3230)

(Experimental) More interface support for multi-measurement and gradient output types 🧪

  • The autograd and Tensorflow interfaces now support devices with shot vectors when qml.enable_return() has been called. (#3374) (#3400)

    Here is an example using Tensorflow:

    import tensorflow as tf
    qml.enable_return()
    
    dev = qml.device("default.qubit", wires=2, shots=[1000, 2000, 3000])
    
    @qml.qnode(dev, diff_method="parameter-shift", interface="tf")
    def circuit(a):
        qml.RY(a, wires=0)
        qml.RX(0.2, wires=0)
        qml.CNOT(wires=[0, 1])
        return qml.expval(qml.PauliZ(0)), qml.probs([0, 1])
    
    >>> a = tf.Variable(0.4)
    >>> with tf.GradientTape() as tape:
    ...     res = circuit(a)
    ...     res = tf.stack([tf.experimental.numpy.hstack(r) for r in res])
    ...
    >>> res
    <tf.Tensor: shape=(3, 5), dtype=float64, numpy=
    array([[0.902, 0.951, 0.   , 0.   , 0.049],
           [0.898, 0.949, 0.   , 0.   , 0.051],
           [0.892, 0.946, 0.   , 0.   , 0.054]])>
    >>> tape.jacobian(res, a)
    <tf.Tensor: shape=(3, 5), dtype=float64, numpy=
    array([[-0.345     , -0.1725    ,  0.        ,  0.        ,  0.1725    ],
           [-0.383     , -0.1915    ,  0.        ,  0.        ,  0.1915    ],
           [-0.38466667, -0.19233333,  0.        ,  0.        ,  0.19233333]])>
    
  • The PyTorch interface is now fully supported when qml.enable_return() has been called, allowing the calculation of the Jacobian and the Hessian using custom differentiation methods (e.g., parameter-shift, finite difference, or adjoint). (#3416)

    import torch
    
    qml.enable_return()
    
    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev, diff_method="parameter-shift", interface="torch")
    def circuit(a, b):
        qml.RY(a, wires=0)
        qml.RX(b, wires=1)
        qml.CNOT(wires=[0, 1])
        return qml.expval(qml.PauliZ(0)), qml.probs([0, 1])
    
    >>> a = torch.tensor(0.1, requires_grad=True)
    >>> b = torch.tensor(0.2, requires_grad=True)
    >>> torch.autograd.functional.jacobian(circuit, (a, b))
    ((tensor(-0.0998), tensor(0.)), (tensor([-0.0494, -0.0005,  0.0005,  0.0494]), tensor([-0.0991,  0.0991,  0.0002, -0.0002])))
    
  • The JAX-JIT interface now supports first-order gradient computation when qml.enable_return() has been called. (#3235) (#3445)

    import jax
    from jax import numpy as jnp
    
    jax.config.update("jax_enable_x64", True)
    
    qml.enable_return()
    
    dev = qml.device("lightning.qubit", wires=2)
    
    @jax.jit
    @qml.qnode(dev, interface="jax-jit", diff_method="parameter-shift")
    def circuit(a, b):
        qml.RY(a, wires=0)
        qml.RX(b, wires=0)
        return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))
    
    a, b = jnp.array(1.0), jnp.array(2.0)
    
    >>> jax.jacobian(circuit, argnums=[0, 1])(a, b)
    ((Array(0.35017549, dtype=float64, weak_type=True),
    Array(-0.4912955, dtype=float64, weak_type=True)),
    (Array(5.55111512e-17, dtype=float64, weak_type=True),
    Array(0., dtype=float64, weak_type=True)))
    

Improvements 🛠

  • qml.pauli.is_pauli_word now supports instances of qml.Hamiltonian. (#3389)

  • When qml.probs, qml.counts, and qml.sample are called with no arguments, they measure all wires. Calling any of the aforementioned measurements with an empty wire list (e.g., qml.sample(wires=[])) will raise an error. (#3299)

  • Made qml.gradients.finite_diff more convenient to use with custom data type observables/devices by reducing the number of magic methods that need to be defined in the custom data type to support finite_diff. (#3426)

  • The qml.ISWAP gate is now natively supported on default.mixed, improving on its efficiency. (#3284)

  • Added more input validation to qml.transforms.hamiltonian_expand such that Hamiltonian objects with no terms raise an error. (#3339)

  • Continuous integration checks are now performed for Python 3.11 and Torch v1.13. Python 3.7 is dropped. (#3276)

  • qml.Tracker now also logs results in tracker.history when tracking the execution of a circuit.

    (#3306)

  • The execution time of Wires.all_wires has been improved by avoiding data type changes and making use of itertools.chain. (#3302)

  • Printing an instance of qml.qchem.Molecule is now more concise and informational. (#3364)

  • The error message for qml.transforms.insert when it fails to diagonalize non-qubit-wise-commuting observables is now more detailed. (#3381)

  • Extended the qml.equal function to qml.Hamiltonian and Tensor objects. (#3390)

  • QuantumTape._process_queue has been moved to qml.queuing.process_queue to disentangle its functionality from the QuantumTape class. (#3401)

  • QPE can now accept a target operator instead of a matrix and target wires pair. (#3373)

  • The qml.ops.op_math.Controlled.map_wires method now uses base.map_wires internally instead of the private _wires property setter. (#3405)

  • A new function called qml.tape.make_qscript has been created for converting a quantum function into a quantum script. This replaces qml.transforms.make_tape. (#3429)

  • Add a _pauli_rep attribute to operators to integrate the new Pauli arithmetic classes with native PennyLane objects. (#3443)

  • Extended the functionality of qml.matrix to qutrits. (#3508)

  • The qcut.py file in pennylane/transforms/ has been reorganized into multiple files that are now in pennylane/transforms/qcut/. (#3413)

  • A warning now appears when creating a Tensor object with overlapping wires, informing that this can lead to undefined behaviour. (#3459)

  • Extended the qml.equal function to qml.ops.op_math.Controlled and qml.ops.op_math.ControlledOp objects. (#3463)

  • Nearly every instance of with QuantumTape() has been replaced with QuantumScript construction. (#3454)

  • Added validate_subspace static method to qml.Operator to check the validity of the subspace of certain qutrit operations. (#3340)

  • qml.equal now supports operators created via qml.s_prod, qml.pow, qml.exp, and qml.adjoint. (#3471)

  • Devices can now disregard observable grouping indices in Hamiltonians through the optional use_grouping attribute. (#3456)

  • Add the optional argument lazy=True to functions qml.s_prod, qml.prod and qml.op_sum to allow simplification. (#3483)

  • Updated the qml.transforms.zyz_decomposition function such that it now supports broadcast operators. This means that single-qubit qml.QubitUnitary operators, instantiated from a batch of unitaries, can now be decomposed. (#3477)

  • The performance of executing circuits under the jax.vmap transformation has been improved by being able to leverage the batch-execution capabilities of some devices. (#3452)

  • The tolerance for converting openfermion Hamiltonian complex coefficients to real ones has been modified to prevent conversion errors. (#3367)

  • OperationRecorder now inherits from AnnotatedQueue and QuantumScript instead of QuantumTape. (#3496)

  • Updated qml.transforms.split_non_commuting to support the new return types. (#3414)

  • Updated qml.transforms.mitigate_with_zne to support the new return types. (#3415)

  • Updated qml.transforms.metric_tensor, qml.transforms.adjoint_metric_tensor, qml.qinfo.classical_fisher, and qml.qinfo.quantum_fisher to support the new return types. (#3449)

  • Updated qml.transforms.batch_params and qml.transforms.batch_input to support the new return types. (#3431)

  • Updated qml.transforms.cut_circuit and qml.transforms.cut_circuit_mc to support the new return types. (#3346)

  • Limit NumPy version to <1.24. (#3346)

Breaking changes 💔

  • Python 3.7 support is no longer maintained. PennyLane will be maintained for versions 3.8 and up. (#3276)

  • The log_base attribute has been moved from MeasurementProcess to the new VnEntropyMP and MutualInfoMP classes, which inherit from MeasurementProcess. (#3326)

  • qml.utils.decompose_hamiltonian() has been removed. Please use qml.pauli.pauli_decompose() instead. (#3384)

  • The return_type attribute of MeasurementProcess has been removed where possible. Use isinstance checks instead. (#3399)

  • Instead of having an OrderedDict attribute called _queue, AnnotatedQueue now inherits from OrderedDict and encapsulates the queue. Consequentially, this also applies to the QuantumTape class which inherits from AnnotatedQueue. (#3401)

  • The ShadowMeasurementProcess class has been renamed to ClassicalShadowMP. (#3388)

  • The qml.Operation.get_parameter_shift method has been removed. The gradients module should be used for general parameter-shift rules instead. (#3419)

  • The signature of the QubitDevice.statistics method has been changed from

    def statistics(self, observables, shot_range=None, bin_size=None, circuit=None):
    

    to

    def statistics(self, circuit: QuantumTape, shot_range=None, bin_size=None):
    

    (#3421)

  • The MeasurementProcess class is now an abstract class and return_type is now a property of the class. (#3434)

Deprecations 👋

Deprecations cycles are tracked at doc/developement/deprecations.rst.

  • The following methods are deprecated: (#3281)

    • qml.tape.get_active_tape: Use qml.QueuingManager.active_context() instead

    • qml.transforms.qcut.remap_tape_wires: Use qml.map_wires instead

    • qml.tape.QuantumTape.inv(): Use qml.tape.QuantumTape.adjoint() instead

    • qml.tape.stop_recording(): Use qml.QueuingManager.stop_recording() instead

    • qml.tape.QuantumTape.stop_recording(): Use qml.QueuingManager.stop_recording() instead

    • qml.QueuingContext is now qml.QueuingManager

    • QueuingManager.safe_update_info and AnnotatedQueue.safe_update_info: Use update_info instead.

  • qml.transforms.measurement_grouping has been deprecated. Use qml.transforms.hamiltonian_expand instead. (#3417)

  • The observables argument in QubitDevice.statistics is deprecated. Please use circuit instead. (#3433)

  • The seed_recipes argument in qml.classical_shadow and qml.shadow_expval is deprecated. A new argument seed has been added, which defaults to None and can contain an integer with the wanted seed. (#3388)

  • qml.transforms.make_tape has been deprecated. Please use qml.tape.make_qscript instead. (#3478)

Documentation 📝

  • Added documentation on parameter broadcasting regarding both its usage and technical aspects. (#3356)

    The quickstart guide on circuits as well as the the documentation of QNodes and Operators now contain introductions and details on parameter broadcasting. The QNode documentation mostly contains usage details, the Operator documentation is concerned with implementation details and a guide to support broadcasting in custom operators.

  • The return type statements of gradient and Hessian transforms and a series of other functions that are a batch_transform have been corrected. (#3476)

  • Developer documentation for the queuing module has been added. (#3268)

  • More mentions of diagonalizing gates for all relevant operations have been corrected. (#3409)

    The docstrings for compute_eigvals used to say that the diagonalizing gates implemented $U$, the unitary such that $O = U Sigma U^{dagger}$, where $O$ is the original observable and $Sigma$ a diagonal matrix. However, the diagonalizing gates actually implement $U^{dagger}$, since $langle psi | O | psi rangle = langle psi | U Sigma U^{dagger} | psi rangle$, making $U^{dagger} | psi rangle$ the actual state being measured in the $Z$-basis.

  • A warning about using dill to pickle and unpickle datasets has been added. (#3505)

Bug fixes 🐛

  • Fixed a bug that prevented qml.gradients.param_shift from being used for broadcasted tapes. (#3528)

  • Fixed a bug where qml.transforms.hamiltonian_expand didn’t preserve the type of the input results in its output. (#3339)

  • Fixed a bug that made qml.gradients.param_shift raise an error when used with unshifted terms only in a custom recipe, and when using any unshifted terms at all under the new return type system. (#3177)

  • The original tape _obs_sharing_wires attribute is updated during its expansion. (#3293)

  • An issue with drain=False in the adaptive optimizer has been fixed. Before the fix, the operator pool needed to be reconstructed inside the optimization pool when drain=False. With this fix, this reconstruction is no longer needed. (#3361)

  • If the device originally has no shots but finite shots are dynamically specified, Hamiltonian expansion now occurs. (#3369)

  • qml.matrix(op) now fails if the operator truly has no matrix (e.g., qml.Barrier) to match op.matrix(). (#3386)

  • The pad_with argument in the qml.AmplitudeEmbedding template is now compatible with all interfaces. (#3392)

  • Operator.pow now queues its constituents by default. (#3373)

  • Fixed a bug where a QNode returning qml.sample would produce incorrect results when run on a device defined with a shot vector. (#3422)

  • The qml.data module now works as expected on Windows. (#3504)

Contributors ✍️

This release contains contributions from (in alphabetical order):

Guillermo Alonso, Juan Miguel Arrazola, Utkarsh Azad, Samuel Banning, Thomas Bromley, Astral Cai, Albert Mitjans Coma, Ahmed Darwish, Isaac De Vlugt, Olivia Di Matteo, Amintor Dusko, Pieter Eendebak, Lillian M. A. Frederiksen, Diego Guala, Katharine Hyatt, Josh Izaac, Soran Jahangiri, Edward Jiang, Korbinian Kottmann, Christina Lee, Romain Moyard, Lee James O’Riordan, Mudit Pandey, Kevin Shen, Matthew Silverman, Jay Soni, Antal Száva, David Wierichs, Moritz Willmann, and Filippo Vicentini.

orphan

Release 0.27.0

New features since last release

An all-new data module 💾

  • The qml.data module is now available, allowing users to download, load, and create quantum datasets. (#3156)

    Datasets are hosted on Xanadu Cloud and can be downloaded by using qml.data.load():

    >>> H2_datasets = qml.data.load(
    ...   data_name="qchem", molname="H2", basis="STO-3G", bondlength=1.1
    ... )
    >>> H2data = H2_datasets[0]
    >>> H2data
    <Dataset = description: qchem/H2/STO-3G/1.1, attributes: ['molecule', 'hamiltonian', ...]>
    
    • Datasets available to be downloaded can be listed with qml.data.list_datasets().

    • To download or load only specific properties of a dataset, we can specify the desired properties in qml.data.load with the attributes keyword argument:

      >>> H2_hamiltonian = qml.data.load(
      ... data_name="qchem", molname="H2", basis="STO-3G", bondlength=1.1,
      ... attributes=["molecule", "hamiltonian"]
      ... )[0]
      >>> H2_hamiltonian.hamiltonian
      <Hamiltonian: terms=15, wires=[0, 1, 2, 3]>
      

      The available attributes can be found using qml.data.list_attributes():

    • To select data interactively, we can use qml.data.load_interactive():

      >>> qml.data.load_interactive()
      Please select a data name:
          1) qspin
          2) qchem
      Choice [1-2]: 1
      Please select a sysname:
          ...
      Please select a periodicity:
          ...
      Please select a lattice:
          ...
      Please select a layout:
          ...
      Please select attributes:
          ...
      Force download files? (Default is no) [y/N]: N
      Folder to download to? (Default is pwd, will download to /datasets subdirectory):
      
      Please confirm your choices:
      dataset: qspin/Ising/open/rectangular/4x4
      attributes: ['parameters', 'ground_states']
      force: False
      dest folder: datasets
      Would you like to continue? (Default is yes) [Y/n]:
      <Dataset = description: qspin/Ising/open/rectangular/4x4, attributes: ['parameters', 'ground_states']>
      
    • Once a dataset is loaded, its properties can be accessed as follows:

      >>> dev = qml.device("default.qubit",wires=4)
      >>> @qml.qnode(dev)
      ... def circuit():
      ...     qml.BasisState(H2data.hf_state, wires = [0, 1, 2, 3])
      ...     for op in H2data.vqe_gates:
      ...          qml.apply(op)
      ...     return qml.expval(H2data.hamiltonian)
      >>> print(circuit())
      -1.0791430411076344
      

    It’s also possible to create custom datasets with qml.data.Dataset:

    >>> example_hamiltonian = qml.Hamiltonian(coeffs=[1,0.5], observables=[qml.PauliZ(wires=0),qml.PauliX(wires=1)])
    >>> example_energies, _ = np.linalg.eigh(qml.matrix(example_hamiltonian))
    >>> example_dataset = qml.data.Dataset(
    ... data_name = 'Example', hamiltonian=example_hamiltonian, energies=example_energies
    ... )
    >>> example_dataset.data_name
    'Example'
    >>> example_dataset.hamiltonian
      (0.5) [X1]
    + (1) [Z0]
    >>> example_dataset.energies
    array([-1.5, -0.5,  0.5,  1.5])
    

    Custom datasets can be saved and read with the qml.data.Dataset.write() and qml.data.Dataset.read() methods, respectively.

    >>> example_dataset.write('./path/to/dataset.dat')
    >>> read_dataset = qml.data.Dataset()
    >>> read_dataset.read('./path/to/dataset.dat')
    >>> read_dataset.data_name
    'Example'
    >>> read_dataset.hamiltonian
      (0.5) [X1]
    + (1) [Z0]
    >>> read_dataset.energies
    array([-1.5, -0.5,  0.5,  1.5])
    

    We will continue to work on adding more datasets and features for qml.data in future releases.

Adaptive optimization 🏃🏋️🏊

  • Optimizing quantum circuits can now be done adaptively with qml.AdaptiveOptimizer. (#3192)

    The qml.AdaptiveOptimizer takes an initial circuit and a collection of operators as input and adds a selected gate to the circuit at each optimization step. The process of growing the circuit can be repeated until the circuit gradients converge to zero within a given threshold. The adaptive optimizer can be used to implement algorithms such as ADAPT-VQE as shown in the following example.

    Firstly, we define some preliminary variables needed for VQE:

    symbols = ["H", "H", "H"]
    geometry = np.array([[0.01076341, 0.04449877, 0.0],
                        [0.98729513, 1.63059094, 0.0],
                        [1.87262415, -0.00815842, 0.0]], requires_grad=False)
    H, qubits = qml.qchem.molecular_hamiltonian(symbols, geometry, charge = 1)
    

    The collection of gates to grow the circuit is built to contain all single and double excitations:

    n_electrons = 2
    singles, doubles = qml.qchem.excitations(n_electrons, qubits)
    singles_excitations = [qml.SingleExcitation(0.0, x) for x in singles]
    doubles_excitations = [qml.DoubleExcitation(0.0, x) for x in doubles]
    operator_pool = doubles_excitations + singles_excitations
    

    Next, an initial circuit that prepares a Hartree-Fock state and returns the expectation value of the Hamiltonian is defined:

    hf_state = qml.qchem.hf_state(n_electrons, qubits)
    dev = qml.device("default.qubit", wires=qubits)
    @qml.qnode(dev)
    def circuit():
        qml.BasisState(hf_state, wires=range(qubits))
        return qml.expval(H)
    

    Finally, the optimizer is instantiated and then the circuit is created and optimized adaptively:

    opt = qml.optimize.AdaptiveOptimizer()
    for i in range(len(operator_pool)):
        circuit, energy, gradient = opt.step_and_cost(circuit, operator_pool, drain_pool=True)
        print('Energy:', energy)
        print(qml.draw(circuit)())
        print('Largest Gradient:', gradient)
        print()
        if gradient < 1e-3:
            break
    
    Energy: -1.246549938420637
    0: ─╭BasisState(M0)─╭G²(0.20)─┤ ╭<𝓗>
    1: ─├BasisState(M0)─├G²(0.20)─┤ ├<𝓗>
    2: ─├BasisState(M0)─│─────────┤ ├<𝓗>
    3: ─├BasisState(M0)─│─────────┤ ├<𝓗>
    4: ─├BasisState(M0)─├G²(0.20)─┤ ├<𝓗>
    5: ─╰BasisState(M0)─╰G²(0.20)─┤ ╰<𝓗>
    Largest Gradient: 0.14399872776755085
    
    Energy: -1.2613740231529604
    0: ─╭BasisState(M0)─╭G²(0.20)─╭G²(0.19)─┤ ╭<𝓗>
    1: ─├BasisState(M0)─├G²(0.20)─├G²(0.19)─┤ ├<𝓗>
    2: ─├BasisState(M0)─│─────────├G²(0.19)─┤ ├<𝓗>
    3: ─├BasisState(M0)─│─────────╰G²(0.19)─┤ ├<𝓗>
    4: ─├BasisState(M0)─├G²(0.20)───────────┤ ├<𝓗>
    5: ─╰BasisState(M0)─╰G²(0.20)───────────┤ ╰<𝓗>
    Largest Gradient: 0.1349349562423238
    
    Energy: -1.2743971719780331
    0: ─╭BasisState(M0)─╭G²(0.20)─╭G²(0.19)──────────┤ ╭<𝓗>
    1: ─├BasisState(M0)─├G²(0.20)─├G²(0.19)─╭G(0.00)─┤ ├<𝓗>
    2: ─├BasisState(M0)─│─────────├G²(0.19)─│────────┤ ├<𝓗>
    3: ─├BasisState(M0)─│─────────╰G²(0.19)─╰G(0.00)─┤ ├<𝓗>
    4: ─├BasisState(M0)─├G²(0.20)────────────────────┤ ├<𝓗>
    5: ─╰BasisState(M0)─╰G²(0.20)────────────────────┤ ╰<𝓗>
    Largest Gradient: 0.00040841755397108586
    

    For a detailed breakdown of its implementation, check out the Adaptive circuits for quantum chemistry demo.

Automatic interface detection 🧩

  • QNodes now accept an auto interface argument which automatically detects the machine learning library to use. (#3132)

    from pennylane import numpy as np
    import torch
    import tensorflow as tf
    from jax import numpy as jnp
    
    dev = qml.device("default.qubit", wires=2)
    @qml.qnode(dev, interface="auto")
    def circuit(weight):
        qml.RX(weight[0], wires=0)
        qml.RY(weight[1], wires=1)
        return qml.expval(qml.PauliZ(0))
    
    interface_tensors = [[0, 1], np.array([0, 1]), torch.Tensor([0, 1]), tf.Variable([0, 1], dtype=float), jnp.array([0, 1])]
    for tensor in interface_tensors:
        res = circuit(weight=tensor)
        print(f"Result value: {res:.2f}; Result type: {type(res)}")
    
    Result value: 1.00; Result type: <class 'pennylane.numpy.tensor.tensor'>
    Result value: 1.00; Result type: <class 'pennylane.numpy.tensor.tensor'>
    Result value: 1.00; Result type: <class 'torch.Tensor'>
    Result value: 1.00; Result type: <class 'tensorflow.python.framework.ops.EagerTensor'>
    Result value: 1.00; Result type: <class 'jaxlib.xla_extension.Array'>
    

Upgraded JAX-JIT gradient support 🏎

  • JAX-JIT support for computing the gradient of QNodes that return a single vector of probabilities or multiple expectation values is now available. (#3244) (#3261)

    import jax
    from jax import numpy as jnp
    from jax.config import config
    config.update("jax_enable_x64", True)
    
    dev = qml.device("lightning.qubit", wires=2)
    
    @jax.jit
    @qml.qnode(dev, diff_method="parameter-shift", interface="jax")
    def circuit(x, y):
        qml.RY(x, wires=0)
        qml.RY(y, wires=1)
        qml.CNOT(wires=[0, 1])
        return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))
    
    x = jnp.array(1.0)
    y = jnp.array(2.0)
    
    >>> jax.jacobian(circuit, argnums=[0, 1])(x, y)
    (Array([-0.84147098,  0.35017549], dtype=float64, weak_type=True),
     Array([ 4.47445479e-18, -4.91295496e-01], dtype=float64, weak_type=True))
    

    Note that this change depends on jax.pure_callback, which requires jax>=0.3.17.

Construct Pauli words and sentences 🔤

  • We’ve reorganized and grouped everything in PennyLane responsible for manipulating Pauli operators into a pauli module. The grouping module has been deprecated as a result, and logic was moved from pennylane/grouping to pennylane/pauli/grouping. (#3179)

  • qml.pauli.PauliWord and qml.pauli.PauliSentence can be used to represent tensor products and linear combinations of Pauli operators, respectively. These provide a more performant method to compute sums and products of Pauli operators. (#3195)

    • qml.pauli.PauliWord represents tensor products of Pauli operators. We can efficiently multiply and extract the matrix of these operators using this representation.

      >>> pw1 = qml.pauli.PauliWord({0:"X", 1:"Z"})
      >>> pw2 = qml.pauli.PauliWord({0:"Y", 1:"Z"})
      >>> pw1, pw2
      (X(0) @ Z(1), Y(0) @ Z(1))
      >>> pw1 * pw2
      (Z(0), 1j)
      >>> pw1.to_mat(wire_order=[0,1])
      array([[ 0,  0,  1,  0],
            [ 0,  0,  0, -1],
            [ 1,  0,  0,  0],
            [ 0, -1,  0,  0]])
      
    • qml.pauli.PauliSentence represents linear combinations of Pauli words. We can efficiently add, multiply and extract the matrix of these operators in this representation.

      >>> ps1 = qml.pauli.PauliSentence({pw1: 1.2, pw2: 0.5j})
      >>> ps2 = qml.pauli.PauliSentence({pw1: -1.2})
      >>> ps1
      1.2 * X(0) @ Z(1)
      + 0.5j * Y(0) @ Z(1)
      >>> ps1 + ps2
      0.0 * X(0) @ Z(1)
      + 0.5j * Y(0) @ Z(1)
      >>> ps1 * ps2
      -1.44 * I
      + (-0.6+0j) * Z(0)
      >>> (ps1 + ps2).to_mat(wire_order=[0,1])
      array([[ 0. +0.j,  0. +0.j,  0.5+0.j,  0. +0.j],
            [ 0. +0.j,  0. +0.j,  0. +0.j, -0.5+0.j],
            [-0.5+0.j,  0. +0.j,  0. +0.j,  0. +0.j],
            [ 0. +0.j,  0.5+0.j,  0. +0.j,  0. +0.j]])
      

(Experimental) More support for multi-measurement and gradient output types 🧪

  • qml.enable_return() now supports QNodes returning multiple measurements, including shots vectors, and gradient output types. (#2886) (#3052) (#3041) (#3090) (#3069) (#3137) (#3127) (#3099) (#3098) (#3095) (#3091) (#3176) (#3170) (#3194) (#3267) (#3234) (#3232) (#3223) (#3222) (#3315)

    In v0.25, we introduced qml.enable_return(), which separates measurements into their own tensors. The motivation of this change is the deprecation of ragged ndarray creation in NumPy.

    With this release, we’re continuing to elevate this feature by adding support for:

    • Execution (qml.execute)

    • Jacobian vector product (JVP) computation

    • Gradient transforms (qml.gradients.param_shift, qml.gradients.finite_diff, qml.gradients.hessian_transform, qml.gradients.param_shift_hessian).

    • Interfaces (Autograd, TensorFlow, and JAX, although without JIT)

    With this added support, the JAX interface can handle multiple shots (shots vectors), measurements, and gradient output types with qml.enable_return():

    import jax
    
    qml.enable_return()
    dev = qml.device("default.qubit", wires=2, shots=(1, 10000))
    
    params = jax.numpy.array([0.1, 0.2])
    
    @qml.qnode(dev, interface="jax", diff_method="parameter-shift", max_diff=2)
    def circuit(x):
        qml.RX(x[0], wires=[0])
        qml.RY(x[1], wires=[1])
        qml.CNOT(wires=[0, 1])
        return qml.var(qml.PauliZ(0) @ qml.PauliX(1)), qml.probs(wires=[0])
    
    >>> jax.hessian(circuit)(params)
    ((Array([[ 0.,  0.],
                  [ 2., -3.]], dtype=float32),
    Array([[[-0.5,  0. ],
                  [ 0. ,  0. ]],
                [[ 0.5,  0. ],
                  [ 0. ,  0. ]]], dtype=float32)),
    (Array([[ 0.07677898,  0.0563341 ],
                  [ 0.07238522, -1.830669  ]], dtype=float32),
    Array([[[-4.9707499e-01,  2.9999996e-04],
                  [-6.2500127e-04,  1.2500001e-04]],
                  [[ 4.9707499e-01, -2.9999996e-04],
                  [ 6.2500127e-04, -1.2500001e-04]]], dtype=float32)))
    

    For more details, please refer to the documentation.

New basis rotation and tapering features in qml.qchem 🤓

  • Grouped coefficients, observables, and basis rotation transformation matrices needed to construct a qubit Hamiltonian in the rotated basis of molecular orbitals are now calculable via qml.qchem.basis_rotation(). (#3011)

    >>> symbols  = ['H', 'H']
    >>> geometry = np.array([[0.0, 0.0, 0.0], [1.398397361, 0.0, 0.0]], requires_grad = False)
    >>> mol = qml.qchem.Molecule(symbols, geometry)
    >>> core, one, two = qml.qchem.electron_integrals(mol)()
    >>> coeffs, ops, unitaries = qml.qchem.basis_rotation(one, two, tol_factor=1.0e-5)
    >>> unitaries
    [tensor([[-1.00000000e+00, -5.46483514e-13],
           [ 5.46483514e-13, -1.00000000e+00]], requires_grad=True),
    tensor([[-1.00000000e+00,  3.17585063e-14],
            [-3.17585063e-14, -1.00000000e+00]], requires_grad=True),
    tensor([[-0.70710678, -0.70710678],
            [-0.70710678,  0.70710678]], requires_grad=True),
    tensor([[ 2.58789009e-11,  1.00000000e+00],
            [-1.00000000e+00,  2.58789009e-11]], requires_grad=True)]
    
  • Any gate operation can now be tapered according to \(\mathbb{Z}_2\) symmetries of the Hamiltonian via qml.qchem.taper_operation. (#3002) (#3121)

    >>> symbols = ['He', 'H']
    >>> geometry =  np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4589]])
    >>> mol = qml.qchem.Molecule(symbols, geometry, charge=1)
    >>> H, n_qubits = qml.qchem.molecular_hamiltonian(symbols, geometry)
    >>> generators = qml.qchem.symmetry_generators(H)
    >>> paulixops = qml.qchem.paulix_ops(generators, n_qubits)
    >>> paulix_sector = qml.qchem.optimal_sector(H, generators, mol.n_electrons)
    >>> tap_op = qml.qchem.taper_operation(qml.SingleExcitation, generators, paulixops,
    ...                paulix_sector, wire_order=H.wires, op_wires=[0, 2])
    >>> tap_op(3.14159)
    [Exp(1.5707949999999993j PauliY)]
    

    Moreover, the obtained tapered operation can be used directly within a QNode.

    >>> dev = qml.device('default.qubit', wires=[0, 1])
    >>> @qml.qnode(dev)
    ... def circuit(params):
    ...     tap_op(params[0])
    ...     return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
    >>> drawer = qml.draw(circuit, show_all_wires=True)
    >>> print(drawer(params=[3.14159]))
    0: ──Exp(0.00+1.57j Y)─┤ ╭<Z@Z>
    1: ────────────────────┤ ╰<Z@Z>
    
  • Functionality has been added to estimate the number of measurements required to compute an expectation value with a target error and estimate the error in computing an expectation value with a given number of measurements. (#3000)

New functions, operations, and observables 🤩

  • Wires of operators or entire QNodes can now be mapped to other wires via qml.map_wires(). (#3143) (#3145)

    The qml.map_wires() function requires a dictionary representing a wire map. Use it with

    • arbitrary operators:

      >>> op = qml.RX(0.54, wires=0) + qml.PauliX(1) + (qml.PauliZ(2) @ qml.RY(1.23, wires=3))
      >>> op
      (RX(0.54, wires=[0]) + PauliX(wires=[1])) + (PauliZ(wires=[2]) @ RY(1.23, wires=[3]))
      >>> wire_map = {0: 10, 1: 11, 2: 12, 3: 13}
      >>> qml.map_wires(op, wire_map)
      (RX(0.54, wires=[10]) + PauliX(wires=[11])) + (PauliZ(wires=[12]) @ RY(1.23, wires=[13]))
      

      A map_wires method has also been added to operators, which returns a copy of the operator with its wires changed according to the given wire map.

    • entire QNodes:

      dev = qml.device("default.qubit", wires=["A", "B", "C", "D"])
      wire_map = {0: "A", 1: "B", 2: "C", 3: "D"}
      
      @qml.qnode(dev)
      def circuit():
          qml.RX(0.54, wires=0)
          qml.PauliX(1)
          qml.PauliZ(2)
          qml.RY(1.23, wires=3)
          return qml.probs(wires=0)
      
      >>> mapped_circuit = qml.map_wires(circuit, wire_map)
      >>> mapped_circuit()
      tensor([0.92885434, 0.07114566], requires_grad=True)
      >>> print(qml.draw(mapped_circuit)())
      A: ──RX(0.54)─┤  Probs
      B: ──X────────┤
      C: ──Z────────┤
      D: ──RY(1.23)─┤
      
  • The qml.IntegerComparator arithmetic operation is now available. (#3113)

    Given a basis state \(\vert n \rangle\), where \(n\) is a positive integer, and a fixed positive integer \(L\), qml.IntegerComparator flips a target qubit if \(n \geq L\). Alternatively, the flipping condition can be \(n < L\) as demonstrated below:

    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev)
    def circuit():
        qml.BasisState(np.array([0, 1]), wires=range(2))
        qml.broadcast(qml.Hadamard, wires=range(2), pattern='single')
        qml.IntegerComparator(2, geq=False, wires=[0, 1])
        return qml.state()
    
    >>> circuit()
    [-0.5+0.j  0.5+0.j -0.5+0.j  0.5+0.j]
    
  • The qml.GellMann qutrit observable, the ternary generalization of the Pauli observables, is now available. (#3035)

    When using qml.GellMann, the index keyword argument determines which of the 8 Gell-Mann matrices is used.

    dev = qml.device("default.qutrit", wires=2)
    
    @qml.qnode(dev)
    def circuit():
        qml.TClock(wires=0)
        qml.TShift(wires=1)
        qml.TAdd(wires=[0, 1])
        return qml.expval(qml.GellMann(wires=0, index=8) + qml.GellMann(wires=1, index=3))
    
    >>> circuit()
    -0.42264973081037416
    
  • Controlled qutrit operations can now be performed with qml.ControlledQutritUnitary. (#2844)

    The control wires and values that define the operation are defined analogously to the qubit operation.

    dev = qml.device("default.qutrit", wires=3)
    
    @qml.qnode(dev)
    def circuit(U):
        qml.TShift(wires=0)
        qml.TAdd(wires=[0, 1])
        qml.ControlledQutritUnitary(U, control_wires=[0, 1], control_values='12', wires=2)
        return qml.state()
    
    >>> U = np.array([[1, 1, 0], [1, -1, 0], [0, 0, np.sqrt(2)]]) / np.sqrt(2)
    >>> circuit(U)
    tensor([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j,
          0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j,
          0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j,
          0.+0.j, 0.+0.j, 0.+0.j], requires_grad=True)
    

Improvements

  • PennyLane now supports Python 3.11! (#3297)

  • qml.sample and qml.counts work more efficiently and track if computational basis samples are being generated when they are called without specifying an observable. (#3207)

  • The parameters of a basis set containing a different number of Gaussian functions are now easier to differentiate. (#3213)

  • Printing a qml.MultiControlledX operator now shows the control_values keyword argument. (#3113)

  • qml.simplify and transforms like qml.matrix, batch_transform, hamiltonian_expand, and split_non_commuting now work with QuantumScript as well as QuantumTape. (#3209)

  • A redundant flipping of the initial state in the UCCSD and kUpCCGSD templates has been removed. (#3148)

  • qml.adjoint now supports batching if the base operation supports batching. (#3168)

  • qml.OrbitalRotation is now decomposed into two qml.SingleExcitation operations for faster execution and more efficient parameter-shift gradient calculations on devices that natively support qml.SingleExcitation. (#3171)

  • The Exp class decomposes into a PauliRot class if the coefficient is imaginary and the base operator is a Pauli Word. (#3249)

  • Added the operator attributes has_decomposition and has_adjoint that indicate whether a corresponding decomposition or adjoint method is available. (#2986)

  • Structural improvements are made to QueuingManager, formerly QueuingContext, and AnnotatedQueue. (#2794) (#3061) (#3085)

    • QueuingContext is renamed to QueuingManager.

    • QueuingManager should now be the global communication point for putting queuable objects into the active queue.

    • QueuingManager is no longer an abstract base class.

    • AnnotatedQueue and its children no longer inherit from QueuingManager.

    • QueuingManager is no longer a context manager.

    • Recording queues should start and stop recording via the QueuingManager.add_active_queue and QueuingContext.remove_active_queue class methods instead of directly manipulating the _active_contexts property.

    • AnnotatedQueue and its children no longer provide global information about actively recording queues. This information is now only available through QueuingManager.

    • AnnotatedQueue and its children no longer have the private _append, _remove, _update_info, _safe_update_info, and _get_info methods. The public analogues should be used instead.

    • QueuingManager.safe_update_info and AnnotatedQueue.safe_update_info are deprecated. Their functionality is moved to update_info.

  • qml.Identity now accepts multiple wires.

    (#3049)

    >>> id_op = qml.Identity([0, 1])
    >>> id_op.matrix()
    array([[1., 0., 0., 0.],
        [0., 1., 0., 0.],
        [0., 0., 1., 0.],
        [0., 0., 0., 1.]])
    >>> id_op.sparse_matrix()
    <4x4 sparse matrix of type '<class 'numpy.float64'>'
        with 4 stored elements in Compressed Sparse Row format>
    >>> id_op.eigvals()
    array([1., 1., 1., 1.])
    
  • Added unitary_check keyword argument to the constructor of the QubitUnitary class which indicates whether the user wants to check for unitarity of the input matrix or not. Its default value is false. (#3063)

  • Modified the representation of WireCut by using qml.draw_mpl. (#3067)

  • Improved the performance of qml.math.expand_matrix function for dense and sparse matrices. (#3060) (#3064)

  • Added support for sums and products of operator classes with scalar tensors of any interface (NumPy, JAX, Tensorflow, PyTorch…). (#3149)

    >>> s_prod = torch.tensor(4) * qml.RX(1.23, 0)
    >>> s_prod
    4*(RX(1.23, wires=[0]))
    >>> s_prod.scalar
    tensor(4)
    
  • Added overlapping_ops property to the Composite class to improve the performance of the eigvals, diagonalizing_gates and Prod.matrix methods. (#3084)

  • Added the map_wires method to the operators, which returns a copy of the operator with its wires changed according to the given wire map. (#3143)

    >>> op = qml.Toffoli([0, 1, 2])
    >>> wire_map = {0: 2, 2: 0}
    >>> op.map_wires(wire_map=wire_map)
    Toffoli(wires=[2, 1, 0])
    
  • Calling compute_matrix and compute_sparse_matrix of simple non-parametric operations is now faster and more memory-efficient with the addition of caching. (#3134)

  • Added details to the output of Exp.label(). (#3126)

  • qml.math.unwrap no longer creates ragged arrays. Lists remain lists. (#3163)

  • New null.qubit device. The null.qubit performs no operations or memory allocations. (#2589)

  • default.qubit favours decomposition and avoids matrix construction for QFT and GroverOperator at larger qubit numbers. (#3193)

  • qml.ControlledQubitUnitary now has a control_values property. (#3206)

  • Added a new qml.tape.QuantumScript class that contains all the non-queuing behavior of QuantumTape. Now, QuantumTape inherits from QuantumScript as well as AnnotatedQueue. (#3097)

  • Extended the qml.equal function to MeasurementProcesses (#3189)

  • qml.drawer.draw.draw_mpl now accepts a style kwarg to select a style for plotting, rather than calling qml.drawer.use_style(style) before plotting. Setting a style for draw_mpl does not change the global configuration for matplotlib plotting. If no style is passed, the function defaults to plotting with the black_white style. (#3247)

Breaking changes

  • QuantumTape._par_info is now a list of dictionaries, instead of a dictionary whose keys are integers starting from zero. (#3185)

  • QueuingContext has been renamed to QueuingManager. (#3061)

  • Deprecation patches for the return types enum’s location and qml.utils.expand are removed. (#3092)

  • _multi_dispatch functionality has been moved inside the get_interface function. This function can now be called with one or multiple tensors as arguments. (#3136)

    >>> torch_scalar = torch.tensor(1)
    >>> torch_tensor = torch.Tensor([2, 3, 4])
    >>> numpy_tensor = np.array([5, 6, 7])
    >>> qml.math.get_interface(torch_scalar)
    'torch'
    >>> qml.math.get_interface(numpy_tensor)
    'numpy'
    

    _multi_dispatch previously had only one argument which contained a list of the tensors to be dispatched:

    >>> qml.math._multi_dispatch([torch_scalar, torch_tensor, numpy_tensor])
    'torch'
    

    To differentiate whether the user wants to get the interface of a single tensor or multiple tensors, get_interface now accepts a different argument per tensor to be dispatched:

    >>> qml.math.get_interface(*[torch_scalar, torch_tensor, numpy_tensor])
    'torch'
    >>> qml.math.get_interface(torch_scalar, torch_tensor, numpy_tensor)
    'torch'
    
  • Operator.compute_terms is removed. On a specific instance of an operator, op.terms() can be used instead. There is no longer a static method for this. (#3215)

Deprecations

  • QueuingManager.safe_update_info and AnnotatedQueue.safe_update_info are deprecated. Instead, update_info no longer raises errors

    if the object isn’t in the queue. (#3085)

  • qml.tape.stop_recording and QuantumTape.stop_recording have been moved to qml.QueuingManager.stop_recording. The old functions will still be available until v0.29. (#3068)

  • qml.tape.get_active_tape has been deprecated. Use qml.QueuingManager.active_context() instead. (#3068)

  • Operator.compute_terms has been removed. On a specific instance of an operator, use op.terms() instead. There is no longer a static method for this. (#3215)

  • qml.tape.QuantumTape.inv() has been deprecated. Use qml.tape.QuantumTape.adjoint instead. (#3237)

  • qml.transforms.qcut.remap_tape_wires has been deprecated. Use qml.map_wires instead. (#3186)

  • The grouping module qml.grouping has been deprecated. Use qml.pauli or qml.pauli.grouping instead. The module will still be available until v0.28. (#3262)

Documentation

  • The code block in the usage details of the UCCSD template has been updated. (#3140)

  • Added a “Deprecations” page to the developer documentation. (#3093)

  • The example of the qml.FlipSign template has been updated. (#3219)

Bug fixes

  • qml.SparseHamiltonian now validates the size of the input matrix. (#3278)

  • Users no longer see unintuitive errors when inputing sequences to qml.Hermitian. (#3181)

  • The evaluation of QNodes that return either vn_entropy or mutual_info raises an informative error message when using devices that define a vector of shots. (#3180)

  • Fixed a bug that made qml.AmplitudeEmbedding incompatible with JITting. (#3166)

  • Fixed the qml.transforms.transpile transform to work correctly for all two-qubit operations. (#3104)

  • Fixed a bug with the control values of a controlled version of a ControlledQubitUnitary. (#3119)

  • Fixed a bug where qml.math.fidelity(non_trainable_state, trainable_state) failed unexpectedly. (#3160)

  • Fixed a bug where qml.QueuingManager.stop_recording did not clean up if yielded code raises an exception. (#3182)

  • Returning qml.sample() or qml.counts() with other measurements of non-commuting observables now raises a QuantumFunctionError (e.g., return qml.expval(PauliX(wires=0)), qml.sample() now raises an error). (#2924)

  • Fixed a bug where op.eigvals() would return an incorrect result if the operator was a non-hermitian composite operator. (#3204)

  • Fixed a bug where qml.BasisStatePreparation and qml.BasisEmbedding were not jit-compilable with JAX. (#3239)

  • Fixed a bug where qml.MottonenStatePreparation was not jit-compilable with JAX. (#3260)

  • Fixed a bug where qml.MottonenStatePreparation was not jit-compilable with JAX. (#3260)

  • Fixed a bug where qml.expval(qml.Hamiltonian()) would not raise an error if the Hamiltonian involved some wires that are not present on the device. (#3266)

  • Fixed a bug where qml.tape.QuantumTape.shape() did not account for the batch dimension of the tape (#3269)

Contributors

This release contains contributions from (in alphabetical order):

Kamal Mohamed Ali, Guillermo Alonso-Linaje, Juan Miguel Arrazola, Utkarsh Azad, Thomas Bromley, Albert Mitjans Coma, Isaac De Vlugt, Olivia Di Matteo, Amintor Dusko, Lillian M. A. Frederiksen, Diego Guala, Josh Izaac, Soran Jahangiri, Edward Jiang, Korbinian Kottmann, Christina Lee, Romain Moyard, Lee J. O’Riordan, Mudit Pandey, Matthew Silverman, Jay Soni, Antal Száva, David Wierichs,

orphan

Release 0.26.0

New features since last release

Classical shadows 👤

  • PennyLane now provides built-in support for implementing the classical-shadows measurement protocol. (#2820) (#2821) (#2871) (#2968) (#2959) (#2968)

    The classical-shadow measurement protocol is described in detail in the paper Predicting Many Properties of a Quantum System from Very Few Measurements. As part of the support for classical shadows in this release, two new finite-shot and fully-differentiable measurements are available:

    • QNodes returning the new measurement qml.classical_shadow() will return two entities; bits (0 or 1 if the 1 or -1 eigenvalue is sampled, respectively) and recipes (the randomized Pauli measurements that are performed for each qubit, labelled by integer):

      dev = qml.device("default.qubit", wires=2, shots=3)
      
      @qml.qnode(dev)
      def circuit():
          qml.Hadamard(wires=0)
          qml.CNOT(wires=[0, 1])
          return qml.classical_shadow(wires=[0, 1])
      
      >>> bits, recipes = circuit()
      >>> bits
      tensor([[0, 0],
              [1, 0],
              [0, 1]], dtype=uint8, requires_grad=True)
      >>> recipes
      tensor([[2, 2],
              [0, 2],
              [0, 2]], dtype=uint8, requires_grad=True)
      
    • QNodes returning qml.shadow_expval() yield the expectation value estimation using classical shadows:

      dev = qml.device("default.qubit", wires=range(2), shots=10000)
      
      @qml.qnode(dev)
      def circuit(x, H):
          qml.Hadamard(0)
          qml.CNOT((0,1))
          qml.RX(x, wires=0)
          return qml.shadow_expval(H)
      
      x = np.array(0.5, requires_grad=True)
      H = qml.Hamiltonian(
              [1., 1.],
              [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliX(0) @ qml.PauliX(1)]
          )
      
      >>> circuit(x, H)
      tensor(1.8486, requires_grad=True)
      >>> qml.grad(circuit)(x, H)
      -0.4797000000000001
      

    Fully-differentiable QNode transforms for both new classical-shadows measurements are also available via qml.shadows.shadow_state and qml.shadows.shadow_expval, respectively.

    For convenient post-processing, we’ve also added the ability to calculate general Renyi entropies by way of the ClassicalShadow class’ entropy method, which requires the wires of the subsystem of interest and the Renyi entropy order:

    >>> shadow = qml.ClassicalShadow(bits, recipes)
    >>> vN_entropy = shadow.entropy(wires=[0, 1], alpha=1)
    

Qutrits: quantum circuits for tertiary degrees of freedom ☘️

  • An entirely new framework for quantum computing is now simulatable with the addition of qutrit functionalities. (#2699) (#2781) (#2782) (#2783) (#2784) (#2841) (#2843)

    Qutrits are like qubits, but instead live in a three-dimensional Hilbert space; they are not binary degrees of freedom, they are tertiary. The advent of qutrits allows for all sorts of interesting theoretical, practical, and algorithmic capabilities that have yet to be discovered.

    To facilitate qutrit circuits requires a new device: default.qutrit. The default.qutrit device is a Python-based simulator, akin to default.qubit, and is defined as per usual:

    >>> dev = qml.device("default.qutrit", wires=1)
    

    The following operations are supported on default.qutrit devices:

    • The qutrit shift operator, qml.TShift, and the ternary clock operator, qml.TClock, as defined in this paper by Yeh et al. (2022), which are the qutrit analogs of the Pauli X and Pauli Z operations, respectively.

    • The qml.TAdd and qml.TSWAP operations which are the qutrit analogs of the CNOT and SWAP operations, respectively.

    • Custom unitary operations via qml.QutritUnitary.

    • qml.state and qml.probs measurements.

    • Measuring user-specified Hermitian matrix observables via qml.THermitian.

    A comprehensive example of these features is given below:

    dev = qml.device("default.qutrit", wires=1)
    
    U = np.array([
            [1, 1, 1],
            [1, 1, 1],
            [1, 1, 1]
        ]
    ) / np.sqrt(3)
    
    obs = np.array([
            [1, 1, 0],
            [1, -1, 0],
            [0, 0, np.sqrt(2)]
        ]
    ) / np.sqrt(2)
    
    @qml.qnode(dev)
    def qutrit_state(U, obs):
        qml.TShift(0)
        qml.TClock(0)
        qml.QutritUnitary(U, wires=0)
        return qml.state()
    
    @qml.qnode(dev)
    def qutrit_expval(U, obs):
        qml.TShift(0)
        qml.TClock(0)
        qml.QutritUnitary(U, wires=0)
        return qml.expval(qml.THermitian(obs, wires=0))
    
    >>> qutrit_state(U, obs)
    tensor([-0.28867513+0.5j, -0.28867513+0.5j, -0.28867513+0.5j], requires_grad=True)
    >>> qutrit_expval(U, obs)
    tensor(0.80473785, requires_grad=True)
    

    We will continue to add more and more support for qutrits in future releases.

Simplifying just got... simpler 😌

  • The qml.simplify() function has several intuitive improvements with this release. (#2978) (#2982) (#2922) (#3012)

    qml.simplify can now perform the following:

    • simplify parametrized operations

    • simplify the adjoint and power of specific operators

    • group like terms in a sum

    • resolve products of Pauli operators

    • combine rotation angles of identical rotation gates

    Here is an example of qml.simplify in action with parameterized rotation gates. In this case, the angles of rotation are simplified to be modulo \(4\pi\).

    >>> op1 = qml.RX(30.0, wires=0)
    >>> qml.simplify(op1)
    RX(4.867258771281655, wires=[0])
    >>> op2 = qml.RX(4 * np.pi, wires=0)
    >>> qml.simplify(op2)
    Identity(wires=[0])
    

    All of these simplification features can be applied directly to quantum functions, QNodes, and tapes via decorating with @qml.simplify, as well:

    dev = qml.device("default.qubit", wires=2)
    @qml.simplify
    @qml.qnode(dev)
    def circuit():
        qml.adjoint(qml.prod(qml.RX(1, 0) ** 1, qml.RY(1, 0), qml.RZ(1, 0)))
        return qml.probs(wires=0)
    
    >>> circuit()
    >>> list(circuit.tape)
    [RZ(11.566370614359172, wires=[0]) @ RY(11.566370614359172, wires=[0]) @ RX(11.566370614359172, wires=[0]),
     probs(wires=[0])]
    

QNSPSA optimizer 💪

  • A new optimizer called qml.QNSPSAOptimizer is available that implements the quantum natural simultaneous perturbation stochastic approximation (QNSPSA) method based on Simultaneous Perturbation Stochastic Approximation of the Quantum Fisher Information. (#2818)

    qml.QNSPSAOptimizer is a second-order SPSA algorithm, which combines the convergence power of the quantum-aware Quantum Natural Gradient (QNG) optimization method with the reduced quantum evaluations of SPSA methods.

    While the QNSPSA optimizer requires additional circuit executions (10 executions per step) compared to standard SPSA optimization (3 executions per step), these additional evaluations are used to provide a stochastic estimation of a second-order metric tensor, which often helps the optimizer to achieve faster convergence.

    Use qml.QNSPSAOptimizer like you would any other optimizer:

    max_iterations = 50
    opt = qml.QNSPSAOptimizer()
    
    for _ in range(max_iterations):
        params, cost = opt.step_and_cost(cost, params)
    

    Check out our demo on the QNSPSA optimizer for more information.

Operator and parameter broadcasting supplements 📈

  • Operator methods for exponentiation and raising to a power have been added. (#2799) (#3029)

    • The qml.exp function can be used to create observables or generic rotation gates:

      >>> x = 1.234
      >>> t = qml.PauliX(0) @ qml.PauliX(1) + qml.PauliY(0) @ qml.PauliY(1)
      >>> isingxy = qml.exp(t, 0.25j * x)
      >>> isingxy.matrix()
      array([[1.       +0.j        , 0.       +0.j        ,
          1.       +0.j        , 0.       +0.j        ],
         [0.       +0.j        , 0.8156179+0.j        ,
          1.       +0.57859091j, 0.       +0.j        ],
         [0.       +0.j        , 0.       +0.57859091j,
          0.8156179+0.j        , 0.       +0.j        ],
         [0.       +0.j        , 0.       +0.j        ,
          1.       +0.j        , 1.       +0.j        ]])
      
    • The qml.pow function raises a given operator to a power:

      >>> op = qml.pow(qml.PauliX(0), 2)
      >>> op.matrix()
      array([[1, 0], [0, 1]])
      
  • An operator called qml.PSWAP is now available. (#2667)

    The qml.PSWAP gate – or phase-SWAP gate – was previously available within the PennyLane-Braket plugin only. Enjoy it natively in PennyLane with v0.26.

  • Check whether or not an operator is hermitian or unitary with qml.is_hermitian and qml.is_unitary. (#2960)

    >>> op1 = qml.PauliX(wires=0)
    >>> qml.is_hermitian(op1)
    True
    >>> op2 = qml.PauliX(0) + qml.RX(np.pi/3, 0)
    >>> qml.is_unitary(op2)
    False
    
  • Embedding templates now support parameter broadcasting. (#2810)

    Embedding templates like AmplitudeEmbedding or IQPEmbedding now support parameter broadcasting with a leading broadcasting dimension in their variational parameters. AmplitudeEmbedding, for example, would usually use a one-dimensional input vector of features. With broadcasting, we can now compute

    >>> features = np.array([
    ...     [0.5, 0.5, 0., 0., 0.5, 0., 0.5, 0.],
    ...     [1., 0., 0., 0., 0., 0., 0., 0.],
    ...     [0.5, 0.5, 0., 0., 0., 0., 0.5, 0.5],
    ... ])
    >>> op = qml.AmplitudeEmbedding(features, wires=[1, 5, 2])
    >>> op.batch_size
    3
    

    An exception is BasisEmbedding, which is not broadcastable.

Improvements

  • The qml.math.expand_matrix() method now allows the sparse matrix representation of an operator to be extended to a larger hilbert space. (#2998)

    >>> from scipy import sparse
    >>> mat = sparse.csr_matrix([[0, 1], [1, 0]])
    >>> qml.math.expand_matrix(mat, wires=[1], wire_order=[0,1]).toarray()
    array([[0., 1., 0., 0.],
           [1., 0., 0., 0.],
           [0., 0., 0., 1.],
           [0., 0., 1., 0.]])
    
  • qml.ctrl now uses Controlled instead of ControlledOperation. The new Controlled class wraps individual Operator‘s instead of a tape. It provides improved representations and integration. (#2990)

  • qml.matrix can now compute the matrix of tapes and QNodes that contain multiple broadcasted operations or non-broadcasted operations after broadcasted ones. (#3025)

    A common scenario in which this becomes relevant is the decomposition of broadcasted operations: the decomposition in general will contain one or multiple broadcasted operations as well as operations with no or fixed parameters that are not broadcasted.

  • Lists of operators are now internally sorted by their respective wires while also taking into account their commutativity property. (#2995)

  • Some methods of the QuantumTape class have been simplified and reordered to improve both readability and performance. (#2963)

  • The qml.qchem.molecular_hamiltonian function is modified to support observable grouping. (#2997)

  • qml.ops.op_math.Controlled now has basic decomposition functionality. (#2938)

  • Automatic circuit cutting has been improved by making better partition imbalance derivations. Now it is more likely to generate optimal cuts for larger circuits. (#2517)

  • By default, qml.counts only returns the outcomes observed in sampling. Optionally, specifying qml.counts(all_outcomes=True) will return a dictionary containing all possible outcomes. (#2889)

    >>> dev = qml.device("default.qubit", wires=2, shots=1000)
    >>>
    >>> @qml.qnode(dev)
    >>> def circuit():
    ...     qml.Hadamard(wires=0)
    ...     qml.CNOT(wires=[0, 1])
    ...     return qml.counts(all_outcomes=True)
    >>> result = circuit()
    >>> result
    {'00': 495, '01': 0, '10': 0,  '11': 505}
    
  • Internal use of in-place inversion is eliminated in preparation for its deprecation. (#2965)

  • Controlled operators now work with qml.is_commuting. (#2994)

  • qml.prod and qml.op_sum now support the sparse_matrix() method. (#3006)

    >>> xy = qml.prod(qml.PauliX(1), qml.PauliY(1))
    >>> op = qml.op_sum(xy, qml.Identity(0))
    >>>
    >>> sparse_mat = op.sparse_matrix(wire_order=[0,1])
    >>> type(sparse_mat)
    <class 'scipy.sparse.csr.csr_matrix'>
    >>> sparse_mat.toarray()
    [[1.+1.j 0.+0.j 0.+0.j 0.+0.j]
    [0.+0.j 1.-1.j 0.+0.j 0.+0.j]
    [0.+0.j 0.+0.j 1.+1.j 0.+0.j]
    [0.+0.j 0.+0.j 0.+0.j 1.-1.j]]
    
  • Provided sparse_matrix() support for single qubit observables. (#2964)

  • qml.Barrier with only_visual=True now simplifies via op.simplify() to the identity operator or a product of identity operators. (#3016)

  • More accurate and intuitive outputs for printing some operators have been added. (#3013)

  • Results for the matrix of the sum or product of operators are stored in a more efficient manner. (#3022)

  • The computation of the (sparse) matrix for the sum or product of operators is now more efficient. (#3030)

  • When the factors of qml.prod don’t share any wires, the matrix and sparse matrix are computed using a kronecker product for improved efficiency. (#3040)

  • qml.grouping.is_pauli_word now returns False for operators that don’t inherit from qml.Observable instead of raising an error. (#3039)

  • Added functionality to iterate over operators created from qml.op_sum and qml.prod. (#3028)

    >>> op = qml.op_sum(qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2))
    >>> len(op)
    3
    >>> op[1]
    PauliY(wires=[1])
    >>> [o.name for o in op]
    ['PauliX', 'PauliY', 'PauliZ']
    

Deprecations

  • In-place inversion is now deprecated. This includes op.inv() and op.inverse=value. Please use qml.adjoint or qml.pow instead. Support for these methods will remain till v0.28. (#2988)

    Don’t use:

    >>> v1 = qml.PauliX(0).inv()
    >>> v2 = qml.PauliX(0)
    >>> v2.inverse = True
    

    Instead use:

    >>> qml.adjoint(qml.PauliX(0))
    Adjoint(PauliX(wires=[0]))
    >>> qml.pow(qml.PauliX(0), -1)
    PauliX(wires=[0])**-1
    >>> qml.pow(qml.PauliX(0), -1, lazy=False)
    PauliX(wires=[0])
    >>> qml.PauliX(0) ** -1
    PauliX(wires=[0])**-1
    

    qml.adjoint takes the conjugate transpose of an operator, while qml.pow(op, -1) indicates matrix inversion. For unitary operators, adjoint will be more efficient than qml.pow(op, -1), even though they represent the same thing.

  • The supports_reversible_diff device capability is unused and has been removed. (#2993)

Breaking changes

  • Measuring an operator that might not be hermitian now raises a warning instead of an error. To definitively determine whether or not an operator is hermitian, use qml.is_hermitian. (#2960)

  • The ControlledOperation class has been removed. This was a developer-only class, so the change should not be evident to any users. It is replaced by Controlled. (#2990)

  • The default execute method for the QubitDevice base class now calls self.statistics with an additional keyword argument circuit, which represents the quantum tape being executed. Any device that overrides statistics should edit the signature of the method to include the new circuit keyword argument. (#2820)

  • The expand_matrix() has been moved from pennylane.operation to pennylane.math.matrix_manipulation (#3008)

  • qml.grouping.utils.is_commuting has been removed, and its Pauli word logic is now part of qml.is_commuting. (#3033)

  • qml.is_commuting has been moved from pennylane.transforms.commutation_dag to pennylane.ops.functions. (#2991)

Documentation

  • Updated the Fourier transform docs to use circuit_spectrum instead of spectrum, which has been deprecated. (#3018)

  • Corrected the docstrings for diagonalizing gates for all relevant operations. The docstrings used to say that the diagonalizing gates implemented \(U\), the unitary such that \(O = U \Sigma U^{\dagger}\), where \(O\) is the original observable and \(\Sigma\) a diagonal matrix. However, the diagonalizing gates actually implement \(U^{\dagger}\), since \(\langle \psi | O | \psi \rangle = \langle \psi | U \Sigma U^{\dagger} | \psi \rangle\), making \(U^{\dagger} | \psi \rangle\) the actual state being measured in the Z-basis. (#2981)

Bug fixes

  • Fixed a bug with qml.ops.Exp operators when the coefficient is autograd but the diagonalizing gates don’t act on all wires. (#3057)

  • Fixed a bug where the tape transform single_qubit_fusion computed wrong rotation angles for specific combinations of rotations. (#3024)

  • Jax gradients now work with a QNode when the quantum function was transformed by qml.simplify. (#3017)

  • Operators that have num_wires = AnyWires or num_wires = AnyWires now raise an error, with certain exceptions, when instantiated with wires=[]. (#2979)

  • Fixed a bug where printing qml.Hamiltonian with complex coefficients raises TypeError in some cases. (#3004)

  • Added a more descriptive error message when measuring non-commuting observables at the end of a circuit with probs, samples, counts and allcounts. (#3065)

Contributors

This release contains contributions from (in alphabetical order):

Juan Miguel Arrazola, Utkarsh Azad, Tom Bromley, Olivia Di Matteo, Isaac De Vlugt, Yiheng Duan, Lillian Marie Austin Frederiksen, Josh Izaac, Soran Jahangiri, Edward Jiang, Ankit Khandelwal, Korbinian Kottmann, Meenu Kumari, Christina Lee, Albert Mitjans Coma, Romain Moyard, Rashid N H M, Zeyue Niu, Mudit Pandey, Matthew Silverman, Jay Soni, Antal Száva, Cody Wang, David Wierichs.

orphan

Release 0.25.1

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

orphan

Release 0.25.0

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 applicable, while also maintaining differentiability. For example:

    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev)
    def circuit(angles):
        qml.prod(qml.PauliZ(0), qml.RY(angles[0], 1))
        qml.op_sum(qml.PauliX(1), qml.RY(angles[1], 0))
    
        return qml.expval(qml.op_sum(qml.PauliX(0), qml.PauliZ(1)))
    
    >>> angles = np.array([1.23, 4.56], requires_grad=True)
    >>> circuit(angles)
    tensor(0.33423773, requires_grad=True)
    >>> qml.grad(circuit)(angles)
    array([-0.9424888,  0.       ])
    
  • All PennyLane operators can now be added, subtracted, multiplied, scaled, and raised to powers using +, -, @, *, **, respectively. (#2849) (#2825) (#2891)

    • You can now add scalars to operators, where the interpretation is that the scalar is a properly-sized identity matrix;

      >>> sum_op = 5 + qml.PauliX(0)
      >>> sum_op.matrix()
      array([[5., 1.],
             [1., 5.]])
      
    • The + and - operators can be used to combine all Pennylane operators:

      >>> sum_op = qml.RX(phi=1.23, wires=0) + qml.RZ(phi=3.14, wires=0) - qml.RY(phi=0.12, wires=0)
      >>> sum_op
      RX(1.23, wires=[0]) + RZ(3.14, wires=[0]) + -1*(RY(0.12, wires=[0]))
      >>> qml.matrix(sum_op)
      array([[-0.18063077-0.99999968j,  0.05996401-0.57695852j],
             [-0.05996401-0.57695852j, -0.18063077+0.99999968j]])
      

      Note that the behavior of + and - with observables is different; it still creates a Hamiltonian.

    • The * and @ operators can be used to scale and compose all PennyLane operators.

      >>> prod_op = 2*qml.RX(1, wires=0) @ qml.RY(2, wires=0)
      >>> prod_op
      2*(RX(1, wires=[0])) @ RY(2, wires=[0])
      >>> qml.matrix(prod_op)
      array([[ 0.94831976-0.80684536j, -1.47692053-0.51806945j],
             [ 1.47692053-0.51806945j,  0.94831976+0.80684536j]])
      
    • The ** operator can be used to raise PennyLane operators to a power.

      >>> exp_op = qml.RZ(1.0, wires=0) ** 2
      >>> exp_op
      RZ**2(1.0, wires=[0])
      >>> qml.matrix(exp_op)
      array([[0.54030231-0.84147098j, 0.        +0.j        ],
             [0.        +0.j        , 0.54030231+0.84147098j]])
      
  • A new class called Controlled is available in qml.ops.op_math to represent a controlled version of any operator. This will eventually be integrated into qml.ctrl to provide a performance increase and more feature coverage. (#2634)

  • Arithmetic operations can now be simplified using qml.simplify. (#2835) (#2854)

    >>> op = qml.adjoint(qml.adjoint(qml.RX(x, wires=0)))
    >>> op
    Adjoint(Adjoint(RX))(tensor([1.04719755, 1.57079633], requires_grad=True), wires=[0])
    >>> qml.simplify(op)
    RX(tensor([1.04719755, 1.57079633], requires_grad=True), wires=[0])
    
  • A new function called qml.equal can be used to compare the equality of parametric operators. (#2651)

    >>> qml.equal(qml.RX(1.23, 0), qml.RX(1.23, 0))
    True
    >>> qml.equal(qml.RY(4.56, 0), qml.RY(7.89, 0))
    False
    

Marvelous mixed state features 🙌

  • The default.mixed device now supports backpropagation with the "jax" interface, which can result in significant speedups. (#2754) (#2776)

    dev = qml.device("default.mixed", wires=2)
    
    @qml.qnode(dev, diff_method="backprop", interface="jax")
    def circuit(angles):
        qml.RX(angles[0], wires=0)
        qml.RY(angles[1], wires=1)
        return qml.expval(qml.PauliZ(0) + qml.PauliZ(1))
    
    >>> angles = np.array([np.pi/6, np.pi/5], requires_grad=True)
    >>> qml.grad(circuit)(angles)
    array([-0.8660254 , -0.25881905])
    

    Additionally, quantum channels now support Jax and TensorFlow tensors. This allows quantum channels to be used inside QNodes decorated by tf.function, jax.jit, or jax.vmap.

  • The default.mixed device now supports readout error. (#2786)

    A new keyword argument called readout_prob can be specified when creating a default.mixed device. Any circuits running on a default.mixed device with a finite readout_prob (upper-bounded by 1) will alter the measurements performed at the end of the circuit similarly to how a qml.BitFlip channel would affect circuit measurements:

    >>> dev = qml.device("default.mixed", wires=2, readout_prob=0.1)
    >>> @qml.qnode(dev)
    ... def circuit():
    ...     return qml.expval(qml.PauliZ(0))
    >>> circuit()
    array(0.8)
    

Relative entropy is now available in qml.qinfo 💥

  • The quantum information module now supports computation of relative entropy. (#2772)

    We’ve enabled two cases for calculating the relative entropy:

    • A QNode transform via qml.qinfo.relative_entropy:

      dev = qml.device('default.qubit', wires=2)
      
      @qml.qnode(dev)
      def circuit(param):
          qml.RY(param, wires=0)
          qml.CNOT(wires=[0, 1])
          return qml.state()
      
      >>> relative_entropy_circuit = qml.qinfo.relative_entropy(circuit, circuit, wires0=[0], wires1=[0])
      >>> x, y = np.array(0.4), np.array(0.6)
      >>> relative_entropy_circuit((x,), (y,))
      0.017750012490703237
      
    • Support in qml.math for flexible post-processing:

      >>> rho = np.array([[0.3, 0], [0, 0.7]])
      >>> sigma = np.array([[0.5, 0], [0, 0.5]])
      >>> qml.math.relative_entropy(rho, sigma)
      tensor(0.08228288, requires_grad=True)
      

New measurements, operators, and more! ✨

  • A new measurement called qml.counts is available. (#2686) (#2839) (#2876)

    QNodes with shots != None that return qml.counts will yield a dictionary whose keys are bitstrings representing computational basis states that were measured, and whose values are the corresponding counts (i.e., how many times that computational basis state was measured):

    dev = qml.device("default.qubit", wires=2, shots=1000)
    
    @qml.qnode(dev)
    def circuit():
        qml.Hadamard(wires=0)
        qml.CNOT(wires=[0, 1])
        return qml.counts()
    
    >>> circuit()
    {'00': 495, '11': 505}
    

    qml.counts can also accept observables, where the resulting dictionary is ordered by the eigenvalues of the observable.

    dev = qml.device("default.qubit", wires=2, shots=1000)
    
    @qml.qnode(dev)
    def circuit():
        qml.Hadamard(wires=0)
        qml.CNOT(wires=[0, 1])
        return qml.counts(qml.PauliZ(0)), qml.counts(qml.PauliZ(1))
    
    >>> circuit()
    ({-1: 470, 1: 530}, {-1: 470, 1: 530})
    
  • A new experimental return type for QNodes with multiple measurements has been added. (#2814) (#2815) (#2860)

    QNodes returning a list or tuple of different measurements return an intuitive data structure via qml.enable_return(), where the individual measurements are separated into their own tensors:

    qml.enable_return()
    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev)
    def circuit(x):
        qml.Hadamard(wires=[0])
        qml.CRX(x, wires=[0, 1])
        return (qml.probs(wires=[0]), qml.vn_entropy(wires=[0]), qml.probs(wires=0), qml.expval(wires=1))
    
    >>> circuit(0.5)
    (tensor([0.5, 0.5], requires_grad=True), tensor(0.08014815, requires_grad=True), tensor([0.5, 0.5], requires_grad=True), tensor(0.93879128, requires_grad=True))
    

    In addition, QNodes that utilize this new return type support backpropagation. This new return type can be disabled thereafter via qml.disable_return().

  • An operator called qml.FlipSign is now available. (#2780)

    Mathematically, qml.FlipSign functions as follows: \(\text{FlipSign}(n) \vert m \rangle = (-1)^\delta_{n,m} \vert m \rangle\), where \(\vert m \rangle\) is an arbitrary qubit state and $n$ is a qubit configuration:

    basis_state = [0, 1]
    
    dev = qml.device("default.qubit", wires=2)
    
    @qml.qnode(dev)
    def circuit():
      for wire in list(range(2)):
            qml.Hadamard(wires = wire)
      qml.FlipSign(basis_state, wires = list(range(2)))
      return qml.state()
    
    >>> circuit()
    tensor([ 0.5+0.j  -0.5+0.j 0.5+0.j  0.5+0.j], requires_grad=True)
    
  • The simultaneous perturbation stochastic approximation (SPSA) optimizer is available via qml.SPSAOptimizer. (#2661)

    The SPSA optimizer is suitable for cost functions whose evaluation may involve noise. Use the SPSA optimizer like you would any other optimizer:

    max_iterations = 50
    opt = qml.SPSAOptimizer(maxiter=max_iterations)
    
    for _ in range(max_iterations):
        params, cost = opt.step_and_cost(cost, params)
    

More drawing styles 🎨

  • New PennyLane-inspired sketch and sketch_dark styles are now available for drawing circuit diagram graphics. (#2709)

Improvements 📈

  • default.qubit now natively executes any operation that defines a matrix except for trainable Pow operations. (#2836)

  • Added expm to the qml.math module for matrix exponentiation. (#2890)

  • When adjoint differentiation is requested, circuits are now decomposed so that all trainable operations have a generator. (#2836)

  • A warning is now emitted for qml.state, qml.density_matrix, qml.vn_entropy, and qml.mutual_info when using a device with finite shots or a shot list since these measurements are always analytic. (#2918)

  • The efficiency of the Hartree-Fock workflow has been improved by removing repetitive steps. (#2850)

  • The coefficients of the non-differentiable molecular Hamiltonians generated with openfermion now have requires_grad = False by default. (#2865)

  • Upgraded performance of the compute_matrix method of broadcastable parametric operations. (#2759)

  • Jacobians are now cached with the Autograd interface when using the parameter-shift rule. (#2645)

  • The qml.state and qml.density_matrix measurements now support custom wire labels. (#2779)

  • Add trivial behaviour logic to qml.operation.expand_matrix. (#2785)

  • Added an are_pauli_words_qwc function which checks if certain Pauli words are pairwise qubit-wise commuting. This new function improves performance when measuring hamiltonians with many commuting terms. (#2789)

  • Adjoint differentiation now uses the adjoint symbolic wrapper instead of in-place inversion. (#2855)

Breaking changes 💔

  • The deprecated qml.hf module is removed. Users with code that calls qml.hf can simply replace qml.hf with qml.qchem in most cases, or refer to the qchem documentation and demos for more information. (#2795)

  • default.qubit now uses stopping_condition to specify support for anything with a matrix. To override this behavior in inheriting devices and to support only a specific subset of operations, developers need to override stopping_condition. (#2836)

  • Custom devices inheriting from DefaultQubit or QubitDevice can break due to the introduction of parameter broadcasting. (#2627)

    A custom device should only break if all three following statements hold simultaneously:

    1. The custom device inherits from DefaultQubit, not QubitDevice.

    2. The device implements custom methods in the simulation pipeline that are incompatible with broadcasting (for example expval, apply_operation or analytic_probability).

    3. The custom device maintains the flag "supports_broadcasting": True in its capabilities dictionary or it overwrites Device.batch_transform without applying broadcast_expand (or both).

    The capabilities["supports_broadcasting"] is set to True for DefaultQubit. Typically, the easiest fix will be to change the capabilities["supports_broadcasting"] flag to False for the child device and/or to include a call to broadcast_expand in CustomDevice.batch_transform, similar to how Device.batch_transform calls it.

    Separately from the above, custom devices that inherit from QubitDevice and implement a custom _gather method need to allow for the kwarg axis to be passed to this _gather method.

  • The argument argnum of the function qml.batch_input has been redefined: now it indicates the indices of the batched parameters, which need to be non-trainable, in the quantum tape. Consequently, its default value (set to 0) has been removed. (#2873)

    Before this breaking change, one could call qml.batch_input without any arguments when using batched inputs as the first argument of the quantum circuit.

    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):  # argument `inputs` is batched
        qml.RY(weights[0], wires=0)
        qml.AngleEmbedding(inputs, wires=range(2), rotation="Y")
        qml.RY(weights[1], wires=1)
        return qml.expval(qml.PauliZ(1))
    

    With this breaking change, users must set a value to argnum specifying the index of the batched inputs with respect to all quantum tape parameters. In this example the quantum tape parameters are [ weights[0], inputs, weights[1] ], thus argnum should be set to 1, specifying that inputs is batched:

    dev = qml.device("default.qubit", wires=2, shots=None)
    
    @qml.batch_input(argnum=1)
    @qml.qnode(dev, diff_method="parameter-shift", interface="tf")
    def circuit(inputs, weights):
        qml.RY(weights[0], wires=0)
        qml.AngleEmbedding(inputs, wires=range(2), rotation="Y")
        qml.RY(weights[1], wires=1)
        return qml.expval(qml.PauliZ(1))
    
  • PennyLane now depends on newer versions (>=2.7) of the semantic_version package, which provides an updated API that is incompatible which versions of the package prior to 2.7. If you run into issues relating to this package, please reinstall PennyLane. (#2744) (#2767)

Documentation 📕

  • Added a dedicated docstring for the QubitDevice.sample method. (#2812)

  • Optimization examples of using JAXopt and Optax with the JAX interface have been added. (#2769)

  • Updated IsingXY gate docstring. (#2858)

Bug fixes 🐞

  • Fixes qml.equal so that operators with different inverse properties are not equal. (#2947)

  • Cleans up interactions between operator arithmetic and batching by testing supported cases and adding errors when batching is not supported. (#2900)

  • Fixed a bug where the parameter-shift rule wasn’t defined for qml.kUpCCGSD. (#2913)

  • Reworked the Hermiticity check in qml.Hermitian by using qml.math calls because calling .conj() on an EagerTensor from TensorFlow raised an error. (#2895)

  • Fixed a bug where the parameter-shift gradient breaks when using both custom grad_recipes that contain unshifted terms and recipes that do not contain any unshifted terms. (#2834)

  • Fixed mixed CPU-GPU data-locality issues for the Torch interface. (#2830)

  • Fixed a bug where the parameter-shift Hessian of circuits with untrainable parameters might be computed with respect to the wrong parameters or might raise an error. (#2822)

  • Fixed a bug where the custom implementation of the states_to_binary device method was not used. (#2809)

  • qml.grouping.group_observables now works when individual wire labels are iterable. (#2752)

  • The adjoint of an adjoint now has a correct expand result. (#2766)

  • Fixed the ability to return custom objects as the expectation value of a QNode with the Autograd interface. (#2808)

  • The WireCut operator now raises an error when instantiating it with an empty list. (#2826)

  • Hamiltonians with grouped observables are now allowed to be measured on devices which were transformed using qml.transform.insert(). (#2857)

  • Fixed a bug where qml.batch_input raised an error when using a batched operator that was not located at the beginning of the circuit. In addition, now qml.batch_input raises an error when using trainable batched inputs, which avoids an unwanted behaviour with duplicated parameters. (#2873)

  • Calling qml.equal with nested operators now raises a NotImplementedError. (#2877)

  • Fixed a bug where a non-sensible error message was raised when using qml.counts with shots=False. (#2928)

  • Fixed a bug where no error was raised and a wrong value was returned when using qml.counts with another non-commuting observable. (#2928)

  • Operator Arithmetic now allows Hamiltonian objects to be used and produces correct matrices. (#2957)

Contributors

This release contains contributions from (in alphabetical order):

Juan Miguel Arrazola, Utkarsh Azad, Samuel Banning, Prajwal Borkar, Isaac De Vlugt, Olivia Di Matteo, Kristiyan Dilov, David Ittah, Josh Izaac, Soran Jahangiri, Edward Jiang, Ankit Khandelwal, Korbinian Kottmann, Meenu Kumari, Christina Lee, Sergio Martínez-Losa, Albert Mitjans Coma, Ixchel Meza Chavez, Romain Moyard, Lee James O’Riordan, Mudit Pandey, Bogdan Reznychenko, Shuli Shu, Jay Soni, Modjtaba Shokrian-Zini, Antal Száva, David Wierichs, Moritz Willmann

orphan

Release 0.24.0

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 attributes must be specified:

    • Operator.ndim_params specifies expected number of dimensions for each parameter

    Once set, Operator.batch_size and QuantumTape.batch_size will dynamically compute the parameter broadcasting axis dimension, if present.

Improved JAX JIT support 🏎

  • JAX just-in-time (JIT) compilation now supports vector-valued QNodes, enabling new types of workflows and significant performance boosts. (#2034)

    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)
    
    @jax.jit
    @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])
    
    >>> circuit(x, y)
    Array([0.8397495 , 0.16025047], dtype=float32)
    

    Note that computing the jacobian of vector-valued QNode is not supported with JAX JIT. The output of vector-valued QNodes can, however, be used in the definition of scalar-valued cost functions whose gradients can be computed.

    For example, one can define a cost function that outputs the first element of the probability vector:

    def cost(x, y):
        return circuit(x, y)[0]
    
    >>> jax.grad(cost, argnums=[0])(x, y)
    (Array(-0.2050439, dtype=float32),)
    

More drawing styles 🎨

  • New solarized_light and solarized_dark styles are available for drawing circuit diagram graphics. (#2662)

New operations & transforms 🤖

  • The qml.IsingXY gate is now available (see 1912.04424). (#2649)

  • The qml.ECR (echoed cross-resonance) operation is now available (see 2105.01063). This gate is a maximally-entangling gate and is equivalent to a CNOT gate up to single-qubit pre-rotations. (#2613)

  • The adjoint transform adjoint can now accept either a single instantiated operator or a quantum function. It returns an entity of the same type / call signature as what it was given: (#2222) (#2672)

    >>> qml.adjoint(qml.PauliX(0))
    Adjoint(PauliX)(wires=[0])
    >>> qml.adjoint(qml.RX)(1.23, wires=0)
    Adjoint(RX)(1.23, wires=[0])
    

    Now, adjoint wraps operators in a symbolic operator class qml.ops.op_math.Adjoint. This class should not be constructed directly; the adjoint constructor should always be used instead. The class behaves just like any other Operator:

    >>> op = qml.adjoint(qml.S(0))
    >>> qml.matrix(op)
    array([[1.-0.j, 0.-0.j],
          [0.-0.j, 0.-1.j]])
    >>> qml.eigvals(op)
    array([1.-0.j, 0.-1.j])
    
  • A new symbolic operator class qml.ops.op_math.Pow represents an operator raised to a power. When decomposition() is called, a list of new operators equal to this one raised to the given power is given: (#2621)

    >>> op = qml.ops.op_math.Pow(qml.PauliX(0), 0.5)
    >>> op.decomposition()
    [SX(wires=[0])]
    >>> qml.matrix(op)
    array([[0.5+0.5j, 0.5-0.5j],
         [0.5-0.5j, 0.5+0.5j]])
    
  • A new transform qml.batch_partial is available which behaves similarly to functools.partial, but supports batching in the unevaluated parameters. (#2585)

    This is useful for executing a circuit with a batch dimension in some of its parameters:

    dev = qml.device("default.qubit", wires=1)
    
    @qml.qnode(dev)
    def circuit(x, y):
       qml.RX(x, wires=0)
       qml.RY(y, wires=0)
       return qml.expval(qml.PauliZ(wires=0))
    
    >>> batched_partial_circuit = qml.batch_partial(circuit, x=np.array(np.pi / 4))
    >>> y = np.array([0.2, 0.3, 0.4])
    >>> batched_partial_circuit(y=y)
    tensor([0.69301172, 0.67552491, 0.65128847], requires_grad=True)
    
  • A new transform qml.split_non_commuting is available, which splits a quantum function or tape into multiple functions/tapes determined by groups of commuting observables: (#2587)

    dev = qml.device("default.qubit", wires=1)
    
    @qml.transforms.split_non_commuting
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x,wires=0)
        return [qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(0))]
    
    >>> print(qml.draw(circuit)(0.5))
    0: ──RX(0.50)─┤  <X>
    \
    0: ──RX(0.50)─┤  <Z>
    

Improvements

  • Expectation values of multiple non-commuting observables from within a single QNode are now supported: (#2587)

    >>> 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)
    ...     return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0))
    >>> circuit_rx(0.1, 0.3)
    tensor([ 0.02950279, -0.09537451], requires_grad=True)
    
  • Selecting which parts of parameter-shift Hessians are computed is now possible. (#2538)

    The argnum keyword argument for qml.gradients.param_shift_hessian is now allowed to be a two-dimensional Boolean array_like. Only the indicated entries of the Hessian will then be computed.

    A particularly useful example is the computation of the diagonal of the Hessian:

    dev = qml.device("default.qubit", wires=1)
    
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x[0], wires=0)
        qml.RY(x[1], wires=0)
        qml.RX(x[2], wires=0)
        return qml.expval(qml.PauliZ(0))
    
    argnum = qml.math.eye(3, dtype=bool)
    x = np.array([0.2, -0.9, 1.1], requires_grad=True)
    
    >>> qml.gradients.param_shift_hessian(circuit, argnum=argnum)(x)
    tensor([[-0.09928388,  0.        ,  0.        ],
            [ 0.        , -0.27633945,  0.        ],
            [ 0.        ,  0.        , -0.09928388]], requires_grad=True)
    
  • Commuting Pauli operators are now measured faster. (#2425)

    The logic that checks for qubit-wise commuting (QWC) observables has been improved, resulting in a performance boost that is noticable when many commuting Pauli operators of the same type are measured.

  • It is now possible to add Observable objects to the integer 0, for example qml.PauliX(wires=[0]) + 0. (#2603)

  • Wires can now be passed as the final argument to an Operator, instead of requiring the wires to be explicitly specified with keyword wires. This functionality already existed for Observables, but now extends to all Operators: (#2432)

    >>> qml.S(0)
    S(wires=[0])
    >>> qml.CNOT((0,1))
    CNOT(wires=[0, 1])
    
  • The qml.taper function can now be used to consistently taper any additional observables such as dipole moment, particle number, and spin operators using the symmetries obtained from the Hamiltonian. (#2510)

  • Sparse Hamiltonians’ representation has changed from Coordinate (COO) to Compressed Sparse Row (CSR) format. (#2561)

    The CSR representation is more performant for arithmetic operations and matrix-vector products. This change decreases the expval() calculation time for qml.SparseHamiltonian, specially for large workflows. In addition, the CSR format consumes less memory for qml.SparseHamiltonian storage.

  • IPython now displays the str representation of a Hamiltonian, rather than the repr. This displays more information about the object. (#2648)

  • The qml.qchem tests have been restructured. (#2593) (#2545)

    • OpenFermion-dependent tests are now localized and collected in tests.qchem.of_tests. The new module test_structure is created to collect the tests of the qchem.structure module in one place and remove their dependency to OpenFermion.

    • Test classes have been created to group the integrals and matrices unit tests.

  • An operations_only argument is introduced to the tape.get_parameters method. (#2543)

  • The gradients module now uses faster subroutines and uniform formats of gradient rules. (#2452)

  • Instead of checking types, objects are now processed in the QuantumTape based on a new _queue_category property. This is a temporary fix that will disappear in the future. (#2408)

  • The QNode class now contains a new method best_method_str that returns the best differentiation method for a provided device and interface, in human-readable format. (#2533)

  • Using Operation.inv() in a queuing environment no longer updates the queue’s metadata, but merely updates the operation in place. (#2596)

  • A new method safe_update_info is added to qml.QueuingContext. This method is substituted for qml.QueuingContext.update_info in a variety of places. (#2612) (#2675)

  • BasisEmbedding can accept an int as argument instead of a list of bits. (#2601)

    For example, qml.BasisEmbedding(4, wires = range(4)) is now equivalent to qml.BasisEmbedding([0,1,0,0], wires = range(4)) (as 4==0b100).

  • Introduced a new is_hermitian property to Operators to determine if an operator can be used in a measurement process. (#2629)

  • Added separate requirements_dev.txt for separation of concerns for code development and just using PennyLane. (#2635)

  • The performance of building sparse Hamiltonians has been improved by accumulating the sparse representation of coefficient-operator pairs in a temporary storage and by eliminating unnecessary kron operations on identity matrices. (#2630)

  • Control values are now displayed distinctly in text and matplotlib drawings of circuits. (#2668)

  • The TorchLayer init_method argument now accepts either a torch.nn.init function or a dictionary which should specify a torch.nn.init/torch.Tensor for each different weight. (#2678)

  • The unused keyword argument do_queue for Operation.adjoint is now fully removed. (#2583)

  • Several non-decomposable Adjoint operators are added to the device test suite. (#2658)

  • The developer-facing pow method has been added to Operator with concrete implementations for many classes. (#2225)

  • The ctrl transform and ControlledOperation have been moved to the new qml.ops.op_math submodule. The developer-facing ControlledOperation class is no longer imported top-level. (#2656)

Deprecations

  • qml.ExpvalCost has been deprecated, and usage will now raise a warning. (#2571)

    Instead, it is recommended to simply pass Hamiltonians to the qml.expval function inside QNodes:

    @qml.qnode(dev)
    def ansatz(params):
        some_qfunc(params)
        return qml.expval(Hamiltonian)
    

Breaking changes

  • When using qml.TorchLayer, weights with negative shapes will now raise an error, while weights with size = 0 will result in creating empty Tensor objects. (#2678)

  • PennyLane no longer supports TensorFlow <=2.3. (#2683)

  • The qml.queuing.Queue class has been removed. (#2599)

  • The qml.utils.expand function is now removed; qml.operation.expand_matrix should be used instead. (#2654)

  • The module qml.gradients.param_shift_hessian has been renamed to qml.gradients.parameter_shift_hessian in order to distinguish it from the identically named function. Note that the param_shift_hessian function is unaffected by this change and can be invoked in the same manner as before via the qml.gradients module. (#2528)

  • The properties eigval and matrix from the Operator class were replaced with the methods eigval() and matrix(wire_order=None). (#2498)

  • Operator.decomposition() is now an instance method, and no longer accepts parameters. (#2498)

  • Adds tests, adds no-coverage directives, and removes inaccessible logic to improve code coverage. (#2537)

  • The base classes QubitDevice and DefaultQubit now accept data-types for a statevector. This enables a derived class (device) in a plugin to choose correct data-types: (#2448)

    >>> dev = qml.device("default.qubit", wires=4, r_dtype=np.float32, c_dtype=np.complex64)
    >>> dev.R_DTYPE
    <class 'numpy.float32'>
    >>> dev.C_DTYPE
    <class 'numpy.complex64'>
    

Bug fixes

  • Fixed a bug where returning qml.density_matrix using the PyTorch interface would return a density matrix with wrong shape. (#2643)

  • Fixed a bug to make param_shift_hessian work with QNodes in which gates marked as trainable do not have any impact on the QNode output. (#2584)

  • QNodes can now interpret variations on the interface name, like "tensorflow" or "jax-jit", when requesting backpropagation. (#2591)

  • Fixed a bug for diff_method="adjoint" where incorrect gradients were computed for QNodes with parametrized observables (e.g., qml.Hermitian). (#2543)

  • Fixed a bug where QNGOptimizer did not work with operators whose generator was a Hamiltonian. (#2524)

  • Fixed a bug with the decomposition of qml.CommutingEvolution. (#2542)

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

  • Fixed a bug which caused different behaviour for Hamiltonian @ Observable and Observable @ Hamiltonian. (#2570)

  • Fixed a bug in DiagonalQubitUnitary._controlled where an invalid operation was queued instead of the controlled version of the diagonal unitary. (#2525)

  • Updated the gradients fix to only apply to the strawberryfields.gbs device, since the original logic was breaking some devices. (#2485) (#2595)

  • Fixed a bug in qml.transforms.insert where operations were not inserted after gates within a template. (#2704)

  • Hamiltonian.wires is now properly updated after in place operations. (#2738)

Documentation

  • The centralized Xanadu Sphinx Theme is now used to style the Sphinx documentation. (#2450)

  • Added a reference to qml.utils.sparse_hamiltonian in qml.SparseHamiltonian to clarify how to construct sparse Hamiltonians in PennyLane. (2572)

  • Added a new section in the Gradients and Training page that summarizes the supported device configurations and provides justification. In addition, code examples were added for some selected configurations. (#2540)

  • Added a note for the Depolarization Channel that specifies how the channel behaves for the different values of depolarization probability p. (#2669)

  • The quickstart documentation has been improved. (#2530) (#2534) (#2564 (#2565 (#2566) (#2607) (#2608)

  • The quantum chemistry quickstart documentation has been improved. (#2500)

  • Testing documentation has been improved. (#2536)

  • Documentation for the pre-commit package has been added. (#2567)

  • Documentation for draw control wires change has been updated. (#2682)

Contributors

This release contains contributions from (in alphabetical order):

Guillermo Alonso-Linaje, Mikhail Andrenkov, Juan Miguel Arrazola, Ali Asadi, Utkarsh Azad, Samuel Banning, Avani Bhardwaj, Thomas Bromley, Albert Mitjans Coma, Isaac De Vlugt, Amintor Dusko, Trent Fridey, Christian Gogolin, Qi Hu, Katharine Hyatt, David Ittah, Josh Izaac, Soran Jahangiri, Edward Jiang, Nathan Killoran, Korbinian Kottmann, Ankit Khandelwal, Christina Lee, Chae-Yeun Park, Mason Moreland, Romain Moyard, Maria Schuld, Jay Soni, Antal Száva, tal66, David Wierichs, Roeland Wiersema, WingCode.

orphan

Release 0.23.1

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

orphan

Release 0.23.0

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=[1], u_tape=u_tape)
    tensor(0.999, requires_grad=True)
    

    For more information refer to the documentation of qml.HilbertSchmidt.

More tensor network support 🕸️

  • Adds the qml.MERA template for implementing quantum circuits with the shape of a multi-scale entanglement renormalization ansatz (MERA). (#2418)

    MERA follows the style of previous tensor network templates and is similar to quantum convolutional neural networks.

    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.MERA.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.MERA(range(n_wires),n_block_wires,block, n_params_block, template_weights)
        return qml.expval(qml.PauliZ(wires=1))
    

    It may be necessary to reorder the wires to see the MERA architecture clearly:

    >>> print(qml.draw(circuit,expansion_strategy='device',wire_order=[2,0,1,3])(template_weights))
    2: ───────────────╭C──RY(0.10)──╭X──RY(-0.30)───────────────┤
    0: ─╭X──RY(-0.30)─│─────────────╰C──RY(0.10)──╭C──RY(0.10)──┤
    1: ─╰C──RY(0.10)──│─────────────╭X──RY(-0.30)─╰X──RY(-0.30)─┤  <Z>
    3: ───────────────╰X──RY(-0.30)─╰C──RY(0.10)────────────────┤
    

New transform for transpilation ⚙️

  • Added a swap based transpiler transform. (#2118)

    The transpile function takes a quantum function and a coupling map as inputs and compiles the circuit to ensure that it can be executed on corresponding hardware. The transform can be used as a decorator in the following way:

    dev = qml.device('default.qubit', wires=4)
    
    @qml.qnode(dev)
    @qml.transforms.transpile(coupling_map=[(0, 1), (1, 2), (2, 3)])
    def circuit(param):
        qml.CNOT(wires=[0, 1])
        qml.CNOT(wires=[0, 2])
        qml.CNOT(wires=[0, 3])
        qml.PhaseShift(param, wires=0)
        return qml.probs(wires=[0, 1, 2, 3])
    
    >>> print(qml.draw(circuit)(0.3))
    0: ─╭C───────╭C──────────╭C──Rϕ(0.30)─┤ ╭Probs
    1: ─╰X─╭SWAP─╰X────╭SWAP─╰X───────────┤ ├Probs
    2: ────╰SWAP─╭SWAP─╰SWAP──────────────┤ ├Probs
    3: ──────────╰SWAP────────────────────┤ ╰Probs
    

Improvements

  • QuantumTape objects are now iterable, allowing iteration over the contained operations and measurements. (#2342)

    with qml.tape.QuantumTape() as tape:
        qml.RX(0.432, wires=0)
        qml.RY(0.543, wires=0)
        qml.CNOT(wires=[0, 'a'])
        qml.RX(0.133, wires='a')
        qml.expval(qml.PauliZ(wires=[0]))
    

    Given a QuantumTape object the underlying quantum circuit can be iterated over using a for loop:

    >>> for op in tape:
    ...     print(op)
    RX(0.432, wires=[0])
    RY(0.543, wires=[0])
    CNOT(wires=[0, 'a'])
    RX(0.133, wires=['a'])
    expval(PauliZ(wires=[0]))
    

    Indexing into the circuit is also allowed via tape[i]:

    >>> tape[0]
    RX(0.432, wires=[0])
    

    A tape object can also be converted to a sequence (e.g., to a list) of operations and measurements:

    >>> list(tape)
    [RX(0.432, wires=[0]),
     RY(0.543, wires=[0]),
     CNOT(wires=[0, 'a']),
     RX(0.133, wires=['a']),
     expval(PauliZ(wires=[0]))]
    
  • Added the QuantumTape.shape method and QuantumTape.numeric_type attribute to allow extracting information about the shape and numeric type of the output returned by a quantum tape after execution. (#2044)

    dev = qml.device("default.qubit", wires=2)
    a = np.array([0.1, 0.2, 0.3])
    
    def func(a):
        qml.RY(a[0], wires=0)
        qml.RX(a[1], wires=0)
        qml.RY(a[2], wires=0)
    
    with qml.tape.QuantumTape() as tape:
        func(a)
        qml.state()
    
    >>> tape.shape(dev)
    (1, 4)
    >>> tape.numeric_type
    complex
    
  • Defined a MeasurementProcess.shape method and a MeasurementProcess.numeric_type attribute to allow extracting information about the shape and numeric type of results obtained when evaluating QNodes using the specific measurement process. (#2044)

  • The parameter-shift Hessian can now be computed for arbitrary operations that support the general parameter-shift rule for gradients, using qml.gradients.param_shift_hessian (#2319)

    Multiple ways to obtain the gradient recipe are supported, in the following order of preference:

    • A custom grad_recipe. It is iterated to obtain the shift rule for the second-order derivatives in the diagonal entries of the Hessian.

    • Custom parameter_frequencies. The second-order shift rule can directly be computed using them.

    • An operation’s generator. Its eigenvalues will be used to obtain parameter_frequencies, if they are not given explicitly for an operation.

  • The strategy for expanding a circuit can now be specified with the qml.specs transform, for example to calculate the specifications of the circuit that will actually be executed by the device (expansion_strategy="device"). (#2395)

  • The default.qubit and default.mixed devices now skip over identity operators instead of performing matrix multiplication with the identity. (#2356) (#2365)

  • The function qml.eigvals is modified to use the efficient scipy.sparse.linalg.eigsh method for obtaining the eigenvalues of a SparseHamiltonian. This scipy method is called to compute \(k\) eigenvalues of a sparse \(N \times N\) matrix if k is smaller than \(N-1\). If a larger \(k\) is requested, the dense matrix representation of the Hamiltonian is constructed and the regular qml.math.linalg.eigvalsh is applied. (#2333)

  • The function qml.ctrl was given the optional argument control_values=None. If overridden, control_values takes an integer or a list of integers corresponding to the binary value that each control value should take. The same change is reflected in ControlledOperation. Control values of 0 are implemented by qml.PauliX applied before and after the controlled operation (#2288)

  • Operators now have a has_matrix property denoting whether or not the operator defines a matrix. (#2331) (#2476)

  • Circuit cutting now performs expansion to search for wire cuts in contained operations or tapes. (#2340)

  • The qml.draw and qml.draw_mpl transforms are now located in the drawer module. They can still be accessed via the top-level qml namespace. (#2396)

  • Raise a warning where caching produces identical shot noise on execution results with finite shots. (#2478)

Deprecations

  • The ObservableReturnTypes Sample, Variance, Expectation, Probability, State, and MidMeasure have been moved to measurements from operation. (#2329) (#2481)

Breaking changes

  • The caching ability of devices has been removed. Using the caching on the QNode level is the recommended alternative going forward. (#2443)

    One way for replicating the removed QubitDevice caching behaviour is by creating a cache object (e.g., a dictionary) and passing it to the QNode:

    n_wires = 4
    wires = range(n_wires)
    
    dev = qml.device('default.qubit', wires=n_wires)
    
    cache = {}
    
    @qml.qnode(dev, diff_method='parameter-shift', cache=cache)
    def expval_circuit(params):
        qml.templates.BasicEntanglerLayers(params, wires=wires, rotation=qml.RX)
        return qml.expval(qml.PauliZ(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliZ(3))
    
    shape = qml.templates.BasicEntanglerLayers.shape(5, n_wires)
    params = np.random.random(shape)
    
    >>> expval_circuit(params)
    tensor(0.20598436, requires_grad=True)
    >>> dev.num_executions
    1
    >>> expval_circuit(params)
    tensor(0.20598436, requires_grad=True)
    >>> dev.num_executions
    1
    
  • The qml.finite_diff function has been removed. Please use qml.gradients.finite_diff to compute the gradient of tapes of QNodes. Otherwise, manual implementation is required. (#2464)

  • The get_unitary_matrix transform has been removed, please use qml.matrix instead. (#2457)

  • The update_stepsize method has been removed from GradientDescentOptimizer and its child optimizers. The stepsize property can be interacted with directly instead. (#2370)

  • Most optimizers no longer flatten and unflatten arguments during computation. Due to this change, user provided gradient functions must return the same shape as qml.grad. (#2381)

  • The old circuit text drawing infrastructure has been removed. (#2310)

    • RepresentationResolver was replaced by the Operator.label method.

    • qml.drawer.CircuitDrawer was replaced by qml.drawer.tape_text.

    • qml.drawer.CHARSETS was removed because unicode is assumed to be accessible.

    • Grid and qml.drawer.drawable_grid were removed because the custom data class was replaced by list of sets of operators or measurements.

    • qml.transforms.draw_old was replaced by qml.draw.

    • qml.CircuitGraph.greedy_layers was deleted, as it was no longer needed by the circuit drawer and did not seem to have uses outside of that situation.

    • qml.CircuitGraph.draw was deleted, as we draw tapes instead.

    • The tape method qml.tape.QuantumTape.draw now simply calls qml.drawer.tape_text.

    • In the new pathway, the charset keyword was deleted, the max_length keyword defaults to 100, and the decimals and show_matrices keywords were added.

  • The deprecated QNode, available via qml.qnode_old.QNode, has been removed. Please transition to using the standard qml.QNode. (#2336) (#2337) (#2338)

    In addition, several other components which powered the deprecated QNode have been removed:

    • The deprecated, non-batch compatible interfaces, have been removed.

    • The deprecated tape subclasses QubitParamShiftTape, JacobianTape, CVParamShiftTape, and ReversibleTape have been removed.

  • The deprecated tape execution method tape.execute(device) has been removed. Please use qml.execute([tape], device) instead. (#2339)

Bug fixes

  • Fixed a bug in the qml.PauliRot operation, where computing the generator was not taking into account the operation wires. (#2466)

  • Fixed a bug where non-trainable arguments were shifted in the NesterovMomentumOptimizer if a trainable argument was after it in the argument list. (#2466)

  • Fixed a bug with @jax.jit for grad when diff_method="adjoint" and mode="backward". (#2460)

  • Fixed a bug where qml.DiagonalQubitUnitary did not support @jax.jit and @tf.function. (#2445)

  • Fixed a bug in the qml.PauliRot operation, where computing the generator was not taking into account the operation wires. (#2442)

  • Fixed a bug with the padding capability of AmplitudeEmbedding where the inputs are on the GPU. (#2431)

  • Fixed a bug by adding a comprehensible error message for calling qml.probs without passing wires or an observable. (#2438)

  • The behaviour of qml.about() was modified to avoid warnings being emitted due to legacy behaviour of pip. (#2422)

  • Fixed a bug where observables were not considered when determining the use of the jax-jit interface. (#2427) (#2474)

  • Fixed a bug where computing statistics for a relatively few number of shots (e.g., shots=10), an error arose due to indexing into an array using a Array. (#2427)

  • PennyLane Lightning version in Docker container is pulled from latest wheel-builds. (#2416)

  • Optimizers only consider a variable trainable if they have requires_grad = True. (#2381)

  • Fixed a bug with qml.expval, qml.var, qml.state and qml.probs (when qml.probs is the only measurement) where the dtype specified on the device did not match the dtype of the QNode output. (#2367)

  • Fixed a bug where the output shapes from batch transforms are inconsistent with the QNode output shape. (#2215)

  • Fixed a bug caused by the squeezing in qml.gradients.param_shift_hessian. (#2215)

  • Fixed a bug in which the expval/var of a Tensor(Observable) would depend on the order in which the observable is defined: (#2276)

    >>> @qml.qnode(dev)
    ... def circ(op):
    ...   qml.RX(0.12, wires=0)
    ...   qml.RX(1.34, wires=1)
    ...   qml.RX(3.67, wires=2)
    ...   return qml.expval(op)
    >>> op1 = qml.Identity(wires=0) @ qml.Identity(wires=1) @ qml.PauliZ(wires=2)
    >>> op2 = qml.PauliZ(wires=2) @ qml.Identity(wires=0) @ qml.Identity(wires=1)
    >>> print(circ(op1), circ(op2))
    -0.8636111153905662 -0.8636111153905662
    
  • Fixed a bug where qml.hf.transform_hf() would fail due to missing wires in the qubit operator that is prepared for tapering the HF state. (#2441)

  • Fixed a bug with custom device defined jacobians not being returned properly. (#2485)

Documentation

  • The sections on adding operator and observable support in the “How to add a plugin” section of the plugins page have been updated. (#2389)

  • The missing arXiv reference in the LieAlgebra optimizer has been fixed. (#2325)

Contributors

This release contains contributions from (in alphabetical order):

Karim Alaa El-Din, Guillermo Alonso-Linaje, Juan Miguel Arrazola, Ali Asadi, Utkarsh Azad, Sam Banning, Thomas Bromley, Alain Delgado, Isaac De Vlugt, Olivia Di Matteo, Amintor Dusko, Anthony Hayes, David Ittah, Josh Izaac, Soran Jahangiri, Nathan Killoran, Christina Lee, Angus Lowe, Romain Moyard, Zeyue Niu, Matthew Silverman, Lee James O’Riordan, Maria Schuld, Jay Soni, Antal Száva, Maurice Weber, David Wierichs.

orphan

Release 0.22.2

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.

orphan

Release 0.22.1

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.

orphan

Release 0.22.0

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.9637709+0.j        ]]
    

    Operator transforms can also be used in a functional form:

    >>> x = torch.tensor(0.6, requires_grad=True)
    >>> matrix_fn = qml.matrix(qml.RX)
    >>> matrix_fn(x, wires=[0])
    tensor([[0.9553+0.0000j, 0.0000-0.2955j],
            [0.0000-0.2955j, 0.9553+0.0000j]], grad_fn=<AddBackward0>)
    

    In its functional form, it is fully differentiable with respect to gate arguments:

    >>> loss = torch.real(torch.trace(matrix_fn(x, wires=0)))
    >>> loss.backward()
    >>> x.grad
    tensor(-0.2955)
    

    Some operator transform can also act on multiple operations, by passing quantum functions or tapes:

    >>> def circuit(theta):
    ...     qml.RX(theta, wires=1)
    ...     qml.PauliZ(wires=0)
    >>> qml.matrix(circuit)(np.pi / 4)
    array([[ 0.92387953+0.j,  0.+0.j ,  0.-0.38268343j,  0.+0.j],
    [ 0.+0.j,  -0.92387953+0.j,  0.+0.j,  0. +0.38268343j],
    [ 0. -0.38268343j,  0.+0.j,  0.92387953+0.j,  0.+0.j],
    [ 0.+0.j,  0.+0.38268343j,  0.+0.j,  -0.92387953+0.j]])
    
  • A new transform has been added to construct the pairwise-commutation directed acyclic graph (DAG) representation of a quantum circuit. (#1712)

    In the DAG, each node represents a quantum operation, and edges represent non-commutation between two operations.

    This transform takes into account that not all operations can be moved next to each other by pairwise commutation:

    >>> def circuit(x, y, z):
    ...     qml.RX(x, wires=0)
    ...     qml.RX(y, wires=0)
    ...     qml.CNOT(wires=[1, 2])
    ...     qml.RY(y, wires=1)
    ...     qml.Hadamard(wires=2)
    ...     qml.CRZ(z, wires=[2, 0])
    ...     qml.RY(-y, wires=1)
    ...     return qml.expval(qml.PauliZ(0))
    >>> dag_fn = qml.commutation_dag(circuit)
    >>> dag = dag_fn(np.pi / 4, np.pi / 3, np.pi / 2)
    

    Nodes in the commutation DAG can be accessed via the get_nodes() method, returning a list of the form (ID, CommutationDAGNode):

    >>> nodes = dag.get_nodes()
    >>> nodes
    NodeDataView({0: <pennylane.transforms.commutation_dag.CommutationDAGNode object at 0x7f461c4bb580>, ...}, data='node')
    

    Specific nodes in the commutation DAG can be accessed via the get_node() method:

    >>> second_node = dag.get_node(2)
    >>> second_node
    <pennylane.transforms.commutation_dag.CommutationDAGNode object at 0x136f8c4c0>
    >>> second_node.op
    CNOT(wires=[1, 2])
    >>> second_node.successors
    [3, 4, 5, 6]
    >>> second_node.predecessors
    []
    

Improvements

  • The text-based drawer accessed via qml.draw() has been optimized and improved. (#2128) (#2198)

    The new drawer has:

    • a decimals keyword for controlling parameter rounding

    • a show_matrices keyword for controlling display of matrices

    • a different algorithm for determining positions

    • deprecation of the charset keyword

    • additional minor cosmetic changes

    @qml.qnode(qml.device('lightning.qubit', wires=2))
    def circuit(a, w):
        qml.Hadamard(0)
        qml.CRX(a, wires=[0, 1])
        qml.Rot(*w, wires=[1])
        qml.CRX(-a, wires=[0, 1])
        return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
    
    >>> print(qml.draw(circuit, decimals=2)(a=2.3, w=[1.2, 3.2, 0.7]))
    0: ──H─╭C─────────────────────────────╭C─────────┤ ╭<Z@Z>
    1: ────╰RX(2.30)──Rot(1.20,3.20,0.70)─╰RX(-2.30)─┤ ╰<Z@Z>
    
  • The frequencies of gate parameters are now accessible as an operation property and can be used for circuit analysis, optimization via the RotosolveOptimizer and differentiation with the parameter-shift rule (including the general shift rule). (#2180) (#2182) (#2227)

    >>> op = qml.CRot(0.4, 0.1, 0.3, wires=[0, 1])
    >>> op.parameter_frequencies
    [(0.5, 1.0), (0.5, 1.0), (0.5, 1.0)]
    

    When using qml.gradients.param_shift, either a custom grad_recipe or the parameter frequencies are used to obtain the shift rule for the operation, in that order of preference.

    See Vidal and Theis (2018) and Wierichs et al. (2021) for theoretical background information on the general parameter-shift rule.

  • No two-term parameter-shift rule is assumed anymore by default. (#2227)

    Previously, operations marked for analytic differentiation that did not provide a generator, parameter_frequencies or a custom grad_recipe were assumed to satisfy the two-term shift rule. This now has to be made explicit for custom operations by adding any of the above attributes.

  • Most compilation transforms, and relevant subroutines, have been updated to support just-in-time compilation with jax.jit. (#1894)

  • The qml.draw_mpl transform supports a expansion_strategy keyword argument. (#2271)

  • The qml.gradients module has been streamlined and special-purpose functions moved closer to their use cases, while preserving existing behaviour. (#2200)

  • Added a new partition_pauli_group function to the grouping module for efficiently measuring the N-qubit Pauli group with 3 ** N qubit-wise commuting terms. (#2185)

  • The Operator class has undergone a major refactor with the following changes:

    • Matrices: the static method Operator.compute_matrices() defines the matrix representation of the operator, and the function qml.matrix(op) computes this for a given instance. (#1996)

    • Eigvals: the static method Operator.compute_eigvals() defines the matrix representation of the operator, and the function qml.eigvals(op) computes this for a given instance. (#2048)

    • Decompositions: the static method Operator.compute_decomposition() defines the matrix representation of the operator, and the method op.decomposition() computes this for a given instance. (#2024) (#2053)

    • Sparse matrices: the static method Operator.compute_sparse_matrix() defines the sparse matrix representation of the operator, and the method op.sparse_matrix() computes this for a given instance. (#2050)

    • Linear combinations of operators: The static method compute_terms(), used for representing the linear combination of coefficients and operators representing the operator, has been added. The method op.terms() computes this for a given instance. Currently, only the Hamiltonian class overwrites compute_terms() to store coefficients and operators. The Hamiltonian.terms property hence becomes a proper method called by Hamiltonian.terms(). (#2036)

    • Diagonalization: The diagonalizing_gates() representation has been moved to the highest-level Operator class and is therefore available to all subclasses. A condition qml.operation.defines_diagonalizing_gates has been added, which can be used in tape contexts without queueing. In addition, a static compute_diagonalizing_gates method has been added, which is called by default in diagonalizing_gates(). (#1985) (#1993)

    • Error handling has been improved for Operator representations. Custom errors subclassing OperatorPropertyUndefined are raised if a representation has not been defined. This replaces the NotImplementedError and allows finer control for developers. (#2064) (#2287)

    • A Operator.hyperparameters attribute, used for storing operation parameters that are never trainable, has been added to the operator class. (#2017)

    • The string_for_inverse attribute is removed. (#2021)

    • The expand() method was moved from the Operation class to the main Operator class. (#2053) (#2239)

Deprecations

  • There are several important changes when creating custom operations: (#2214) (#2227) (#2030) (#2061)

    • The Operator.matrix method has been deprecated and Operator.compute_matrix should be defined instead. Operator matrices should be accessed using qml.matrix(op). If you were previously defining the class method Operator._matrix(), this is a a breaking change — please update your operation to instead overwrite Operator.compute_matrix.

    • The Operator.decomposition method has been deprecated and Operator.compute_decomposition should be defined instead. Operator decompositions should be accessed using Operator.decomposition().

    • The Operator.eigvals method has been deprecated and Operator.compute_eigvals should be defined instead. Operator eigenvalues should be accessed using qml.eigvals(op).

    • The Operator.generator property is now a method, and should return an operator instance representing the generator. Note that unlike the other representations above, this is a breaking change. Operator generators should be accessed using qml.generator(op).

    • The Operation.get_parameter_shift method has been deprecated and will be removed in a future release.

      Instead, the functionalities for general parameter-shift rules in the qml.gradients module should be used, together with the operation attributes parameter_frequencies or grad_recipe.

  • Executing tapes using tape.execute(dev) is deprecated. Please use the qml.execute([tape], dev) function instead. (#2306)

  • The subclasses of the quantum tape, including JacobianTape, QubitParamShiftTape, CVParamShiftTape, and ReversibleTape are deprecated. Instead of calling JacobianTape.jacobian() and JacobianTape.hessian(), please use a standard QuantumTape, and apply gradient transforms using the qml.gradients module. (#2306)

  • qml.transforms.get_unitary_matrix() has been deprecated and will be removed in a future release. For extracting matrices of operations and quantum functions, please use qml.matrix(). (#2248)

  • The qml.finite_diff() function has been deprecated and will be removed in an upcoming release. Instead, qml.gradients.finite_diff() can be used to compute purely quantum gradients (that is, gradients of tapes or QNode). (#2212)

  • The MultiControlledX operation now accepts a single wires keyword argument for both control_wires and wires. The single wires keyword should be all the control wires followed by a single target wire. (#2121) (#2278)

Breaking changes

  • The representation of an operator as a matrix has been overhauled. (#1996)

    The “canonical matrix”, which is independent of wires, is now defined in the static method compute_matrix() instead of _matrix. By default, this method is assumed to take all parameters and non-trainable hyperparameters that define the operation.

    >>> qml.RX.compute_matrix(0.5)
    [[0.96891242+0.j         0.        -0.24740396j]
     [0.        -0.24740396j 0.96891242+0.j        ]]
    

    If no canonical matrix is specified for a gate, compute_matrix() raises a MatrixUndefinedError.

  • The generator property has been updated to an instance method, Operator.generator(). It now returns an instantiated operation, representing the generator of the instantiated operator. (#2030) (#2061)

    Various operators have been updated to specify the generator as either an Observable, Tensor, Hamiltonian, SparseHamiltonian, or Hermitian operator.

    In addition, qml.generator(operation) has been added to aid in retrieving generator representations of operators.

  • The argument wires in heisenberg_obs, heisenberg_expand and heisenberg_tr was renamed to wire_order to be consistent with other matrix representations. (#2051)

  • The property kraus_matrices has been changed to a method, and _kraus_matrices renamed to compute_kraus_matrices, which is now a static method. (#2055)

  • The pennylane.measure module has been renamed to pennylane.measurements. (#2236)

Bug fixes

  • The basis property of qml.SWAP was set to "X", which is incorrect; it is now set to None. (#2287)

  • The qml.RandomLayers template now decomposes when the weights are a list of lists. (#2266)

  • The qml.QubitUnitary operation now supports just-in-time compilation using JAX. (#2249)

  • Fixes a bug in the JAX interface where Array objects were not being converted to NumPy arrays before executing an external device. (#2255)

  • The qml.ctrl transform now works correctly with gradient transforms such as the parameter-shift rule. (#2238)

  • Fixes a bug in which passing required arguments into operations as keyword arguments would throw an error because the documented call signature didn’t match the function definition. (#1976)

  • The operation OrbitalRotation previously was wrongfully registered to satisfy the four-term parameter shift rule. The correct eight-term rule will now be used when using the parameter-shift rule. (#2180)

  • Fixes a bug where qml.gradients.param_shift_hessian would produce an error whenever all elements of the Hessian are known in advance to be 0. (#2299)

Documentation

  • The developer guide on adding templates and the architecture