Skip to content

Releases: PennyLaneAI/pennylane

Release 0.43.0

15 Oct 18:03
5dc163b

Choose a tag to compare

New features since last release

A brand new resource estimation module 📖

A new toolkit dedicated to resource estimation is now available in the pennylane.estimator module! The functionality therein is designed to rapidly and flexibly estimate the quantum resources required to execute programs written at different levels of abstraction. This new module includes the following features:

  • A new pennylane.estimator.estimate.estimate function allows users to estimate the quantum resources required to execute a circuit or operation with respect to a given gate set and configuration. (#8203) (#8205) (#8275) (#8227) (#8279) (#8288) (#8311) (#8313) (#8360)

    The pennylane.estimator.estimate.estimate function can be used on circuits written at different levels of detail to get high-level estimates of gate counts and additional wires fast. For workflows that are already defined in detail, like executable QNodes, the pennylane.estimator.estimate.estimate function works as follows:

    import pennylane as qml
    import pennylane.estimator as qre
    
    dev = qml.device("null.qubit")
    
    @qml.qnode(dev)
    def circ():
        for w in range(2):
            qml.Hadamard(wires=w)
        qml.CNOT(wires=[0,1])
        qml.RX(1.23*np.pi, wires=0)
        qml.RY(1.23*np.pi, wires=1)
        qml.QFT(wires=[0, 1, 2])
        return qml.state()
    >>> res = qre.estimate(circ)()
    >>> print(res)
    --- Resources: ---
      Total wires: 3
      algorithmic wires: 3
      allocated wires: 0
        zero state: 0
        any state: 0
      Total gates : 408
      'T': 396,
      'CNOT': 9,
      'Hadamard': 3

    If exact argument values and other details to operators are unknown or not available, pennylane.estimator.estimate.estimate can also be used on new lightweight representations of PennyLane operations that require minimal information to obtain high-level estimates. As part of this release, many operations in PennyLane now have a corresponding lightweight version that inherits from a new class called pennylane.estimator.resource_operator.ResourceOperator, which can be found in the pennylane.estimator module.

    For example, the lightweight representation of QFT is qre.QFT. By simply specifying the number of wires it acts on, we can obtain resource estimates:

    >>> qft = qre.QFT(num_wires=3)
    >>> res = qre.estimate(qft)
    >>> print(res)
    --- Resources: ---
      Total wires: 3
        algorithmic wires: 3
        allocated wires: 0
          zero state: 0
          any state: 0
      Total gates : 408
      'T': 396,
      'CNOT': 9,
      'Hadamard': 3

    One can create a circuit comprising these operations with similar syntax as defining a QNode, but with far less detail. Here is an example of a circuit with 50 (logical) algorithmic qubits, which includes a pennylane.estimator.templates.QROMStatePreparation acting on 48 qubits. Defining this state preparation for execution would require a state vector of length $2^{48}$ (see qml.QROMStatePreparation), but we are able to estimate the required resources with only metadata, bypassing this computational barrier. Even at this scale, the resource estimate is computed in a fraction of a second!

    def my_circuit():
        qre.QROMStatePreparation(num_state_qubits=48)
        for w in range(2):
            qre.Hadamard(wires=w)
        qre.QROM(num_bitstrings=32, size_bitstring=8, restored=False)
        qre.CNOT(wires=[0,1])
        qre.RX(wires=0)
        qre.RY(wires=1)
        qre.QFT(num_wires=30)
        return
    >>> res = qre.estimate(my_circuit)()
    >>> print(res)
    --- Resources: ---
     Total wires: 129
      algorithmic wires: 50
      allocated wires: 79
        zero state: 71
        any state: 8
      Total gates : 2.702E+16
      'Toffoli': 1.126E+15,
      'T': 5.751E+4,
      'CNOT': 2.027E+16,
      'X': 2.252E+15,
      'Z': 32,
      'S': 64,
      'Hadamard': 3.378E+15

    Here is a summary of the lightweight operations made available in this release. A complete list can be found in the pennylane.estimator module.

    • pennylane.estimator.ops.Identity, pennylane.estimator.ops.GlobalPhase, and various non-parametric operators and single-qubit parametric operators. (#8240) (#8242) (#8302)
    • Various controlled single and multi qubit operators. (#8243)
    • pennylane.estimator.ops.Controlled, and pennylane.estimator.ops.Adjoint as symbolic operators. (#8252) (#8349)
    • pennylane.estimator.ops.Pow, pennylane.estimator.ops.Prod, pennylane.estimator.ops.ChangeOpBasis, and parametric multi-qubit operators. (#8255)
    • Templates including pennylane.estimator.templates.SemiAdder, pennylane.estimator.templates.QFT, pennylane.estimator.templates.AQFT, pennylane.estimator.templates.BasisRotation, pennylane.estimator.templates.Select, pennylane.estimator.templates.QROM, pennylane.estimator.templates.SelectPauliRot, pennylane.estimator.templates.QubitUnitary, pennylane.estimator.templates.ControlledSequence, pennylane.estimator.templates.QPE, pennylane.estimator.templates.IterativeQPE, pennylane.estimator.templates.MPSPrep, pennylane.estimator.templates.QROMStatePreparation, pennylane.estimator.templates.UniformStatePrep, pennylane.estimator.templates.AliasSampling, pennylane.estimator.templates.IntegerComparator, pennylane.estimator.templates.SingleQubitComparator, pennylane.estimator.templates.TwoQubitComparator, pennylane.estimator.templates.RegisterComparator, pennylane.estimator.templates.SelectTHC, pennylane.estimator.templates.PrepTHC, and pennylane.estimator.templates.QubitizeTHC. (#8300) (#8305) (#8309)

    For defining your own customized lightweight resource operations that integrate with features in the pennylane.estimator module, check out the documentation for pennylane.estimator.resource_operator.ResourceOperator.

  • Users can define customized configurations to be used during resource estimation using the new pennylane.estimator.resource_config.ResourceConfig class. This enables the seamless analysis of tradeoffs between resources required and quantities like individual gate precisions or different gate decompositions. (#8259)

    In the following example, a pennylane.estimator.resource_config.ResourceConfig is used to modify the default precision of single qubit rotations, and T counts are compared between different configurations.

    def my_circuit():
        qre.RX(wires=0)
        qre.RY(wires=1)
        qre.RZ(wires=2)
        return
    
    my_rc = qre.ResourceConfig()
    res1 = qre.estimate(my_circuit, config=my_rc)()
    my_rc.set_single_qubit_rot_precision(1e-2)
    res2 = qre.estimate(my_circuit, config=my_rc)()
    >>> t1 = res1.gate_counts['T']
    >>> t2 = res2.gate_counts['T']
    >>> print(t1, t2)
    132 51
  • Hamiltonians are often both expensive to compute and to analyze, but the amount of information required to estimate the resources of Hamiltonian simulation can be surprisingly small in comparison. The pennylane.estimator.compact_hamiltonian.CDFHamiltonian, pennylane.estimator.compact_hamiltonian.THCHamiltonian, pennylane.estimator.compact_hamiltonian.VibronicHamiltonian, and pennylane.estimator.compact_hamiltonian.VibrationalHamiltonian classes were added to store the metadata of the Hamiltonian of a quantum system pertaining to resource estimation. In addition, several resource templates were added that are related to the Suzuki-Trotter method for Hamiltonian simulation, including pennylane.estimator.templates.TrotterProduct, pennylane.estimator.templates.TrotterCDF, pennylane.estimator.templates.TrotterTHC, pennylane.estimator.templates.TrotterVibronic, and pennylane.estimator.templates.TrotterVibrational. (#8303)

    Here's a simple example of resource estimation for the simulation of a pennylane.estimator.compact_hamiltonian.CDFHamiltonian, where we only need to specify two integer arguments (num_orbitals and num_fragments) to get resource estimates:

    >>> cdf_ham = qre.CDFHamiltonian(num_orbitals=4, num_fragments=4)
    >>> res = qre.estimate(qre.TrotterCDF(cdf_ham, num_steps=1, order=2))
    >>> print(res)
    --- Resources: ---
      Total wires: 8
        algorithmic wires: 8
        allocated wires: 0
          zero state: 0
          any state: 0
      Total gates : 2.238E+4
      'T': 2.075E+4,
      'CNOT': 448,
      'Z': 336,
      'S': 504,
      'Hadamard': 336
  • In addition to the pennylane.estimator.resource_operator.ResourceOperator class mentioned above, the scalability of the resource estimation functionality in this release is owed to the following new internal classes:

    *...

Read more

Release 0.42.3

20 Aug 20:06
63937da

Choose a tag to compare

Bug fixes 🐛

  • Set autoray package upper-bound in pyproject.toml due to breaking changes introduced in v0.8.0.
    (#8111)

Contributors ✍️

This release contains contributions from (in alphabetical order):

Andrija Paurevic.

Release 0.42.2

11 Aug 22:13
c5bcae7

Choose a tag to compare

Bug fixes 🐛

  • Fixed a recursion error when simplifying operators that are raised to integer powers. For example,

    >>> class DummyOp(qml.operation.Operator):
    ...     pass
    >>> (DummyOp(0) ** 2).simplify()
    DummyOp(0) @ DummyOp(0)

    Previously, this would fail with a recursion error.
    (#8061)
    (#8064)

Contributors ✍️

This release contains contributions from (in alphabetical order):

Christina Lee,
Andrija Paurevic.

Release 0.42.1

24 Jul 17:45
b5cc3bf

Choose a tag to compare

Bug fixes 🐛

  • A warning is raised if PennyLane is imported and a version of JAX greater than 0.6.2 is installed.
    (#7949)

Contributors ✍️

This release contains contributions from (in alphabetical order):

Andrija Paurevic.

Release 0.42.0

15 Jul 16:24
2a74df8

Choose a tag to compare

New features since last release

State-of-the-art templates and decompositions 🐝

  • A new decomposition using unary iteration has been added to Select. This state-of-the-art decomposition reduces the T-count significantly, and uses $c-1$ auxiliary wires, where $c$ is the number of control wires of the Select operator. (#7623) (#7744) (#7842)

    Unary iteration leverages auxiliary wires to store intermediate values for reuse among the different multi-controlled operators, avoiding unnecessary recomputation and leading to more efficient decompositions to elementary gates. This decomposition uses a template called TemporaryAND, which was also added in this release (see the next changelog entry).

    This decomposition rule for Select is available when the graph-based decomposition system is enabled via decomposition.enable_graph:

    import pennylane as qml
    from functools import partial
    
    qml.decomposition.enable_graph()

    To demonstrate the resource-efficiency of this new decomposition, let's use transforms.decompose to decompose an instance of Select using the new unary iterator decomposition rule, and further decompose these gates into the Clifford+T gate set using clifford_t_decomposition so that we can count the number of T gates required:

    reg = qml.registers({"targ": 2, "control": 2, "work": 1})
    targ, control, work = (reg[k] for k in reg.keys())
    
    dev = qml.device('default.qubit')
    ops = [qml.X(targ[0]), qml.X(targ[1]), qml.Y(targ[0]), qml.SWAP(targ)]
    
    @qml.clifford_t_decomposition
    @partial(qml.transforms.decompose, gate_set={
            qml.X, qml.CNOT, qml.TemporaryAND, "Adjoint(TemporaryAND)", "CY", "CSWAP"
        }
    )
    @qml.qnode(dev)
    def circuit():
        qml.Select(ops, control=control, work_wires=work)
        return qml.state()
    >>> unary_specs = qml.specs(circuit)()
    >>> print(unary_specs['resources'].gate_types["T"])
    16
    >>> print(unary_specs['resources'].gate_types["Adjoint(T)"])
    13

    Go check out the Unary iterator decomposition section in the Select documentation for more information!

  • A new template called TemporaryAND has been added. TemporaryAND enables more efficient circuit decompositions, such as the newest decomposition of the Select template. (#7472)

    The TemporaryAND operation is a three-qubit gate equivalent to a logical AND operation (or a reversible Toffoli): it assumes that the target qubit is initialized in the |0〉 state, while Adjoint(TemporaryAND) assumes the target qubit will be output into the |0〉 state. For more details, see Fig. 4 in arXiv:1805.03662.

    from functools import partial
    
    dev = qml.device("default.qubit")
    
    @partial(qml.set_shots, shots=1)
    @qml.qnode(dev)
    def circuit():
        # |0000⟩
        qml.X(0) # |1000⟩
        qml.X(1) # |1100⟩
        # The target wire is in state |0>, so we can apply TemporaryAND
        qml.TemporaryAND([0, 1, 2]) # |1110⟩
        qml.CNOT([2, 3]) # |1111⟩
        # The target wire will be in state |0> after adjoint(TemporaryAND) gate is applied
        # so we can apply adjoint(TemporaryAND)
        qml.adjoint(qml.TemporaryAND([0, 1, 2])) # |1101⟩
        return qml.sample(wires=[0, 1, 2, 3])
    >>> print(circuit())
    [1 1 0 1]
  • A new template called SemiAdder has been added, which provides state-of-the-art resource-efficiency (fewer T gates) when performing addition on a quantum computer. (#7494)

    Based on arXiv:1709.06648, SemiAdder performs the plain addition of two integers in the computational basis. Here is an example of performing 3 + 4 = 7 with 5 additional work wires:

    from functools import partial
    
    x = 3
    y = 4
    
    wires = qml.registers({"x": 3, "y": 6, "work": 5})
    
    dev = qml.device("default.qubit")
    
    @partial(qml.set_shots, shots=1)
    @qml.qnode(dev)
    def circuit():
        qml.BasisEmbedding(x, wires=wires["x"])
        qml.BasisEmbedding(y, wires=wires["y"])
        qml.SemiAdder(wires["x"], wires["y"], wires["work"])
        return qml.sample(wires=wires["y"])
    >>> print(circuit()) 
    [0 0 0 1 1 1]

    The result [0 0 0 1 1 1] is the binary representation of 7.

  • A new template called SelectPauliRot is available, which applies a sequence of uniformly controlled rotations on a target qubit. This operator appears frequently in unitary decompositions and block-encoding techniques. (#7206) (#7617)

    As input, SelectPauliRot requires the angles of rotation to be applied to the target qubit for each control register configuration, as well as the control_wires, the target_wire, and the axis of rotation (rot_axis) for which each rotation is performed (the default is the "Z" axis).

    import numpy as np
    angles = np.array([1.0, 2.0, 3.0, 4.0])
    
    wires = qml.registers({"control": 2, "target": 1})
    dev = qml.device("default.qubit", wires=3)
    
    @qml.qnode(dev)
    def circuit():
        qml.SelectPauliRot(
          angles,
          control_wires=wires["control"],
          target_wire=wires["target"],
          rot_axis="Z"
        )
        return qml.state()
    >>> print(qml.draw(circuit, level="device")())
    0: ─────────────────────────╭●───────────────╭●─┤ ╭State
    1: ───────────╭●────────────│──╭●────────────│──┤ ├State
    2: ──RZ(2.50)─╰X──RZ(-0.50)─╰X─╰X──RZ(-1.00)─╰X─┤ ╰State
  • The decompositions of SingleExcitation, SingleExcitationMinus and SingleExcitationPlus have been made more efficient by reducing the number of rotations gates and CNOT, CZ, and CY gates (where applicable). This leads to lower circuit depth when decomposing these gates. (#7771)

QSVT & QSP angle solver for large polynomials 🕸️

Effortlessly perform QSVT and QSP with polynomials of large degrees, using our new iterative angle solver.

  • A new iterative angle solver for QSVT and QSP is available in the poly_to_angles function, designed for angle computation for polynomials with degrees larger than 1000. (6694)

    Simply set angle_solver="iterative" in the poly_to_angles function to use it.

    import numpy as np
    
    # P(x) = x - 0.5 x^3 + 0.25 x^5
    poly = np.array([0, 1.0, 0, -1/2, 0, 1/4])
    
    qsvt_angles = qml.poly_to_angles(poly, "QSVT", angle_solver="iterative")
    >>> print(qsvt_angles)
    [-4.72195208  1.59759022  1.12953398  1.12953403  1.59759046 -0.00956271]

    This functionality can also be accessed directly from qml.qsvt with the same keyword argument:

    # P(x) = -x + 0.5 x^3 + 0.5 x^5
    poly = np.array([0, -1, 0, 0.5, 0, 0.5])
    
    hamiltonian = qml.dot([0.3, 0.7], [qml.Z(1), qml.X(1) @ qml.Z(2)])
    
    dev = qml.device("default.qubit")
    @qml.qnode(dev)
    def circuit():
        qml.qsvt(
            hamiltonian, poly, encoding_wires=[0], block_encoding="prepselprep", angle_solver="iterative"
        )
        return qml.state()
    
    matrix = qml.matrix(circuit, wire_order=[0, 1, 2])()
    >>> print(matrix[:4, :4].real)
    [[-0.16253996  0.         -0.37925991  0.        ]
     [ 0.         -0.16253996  0.          0.37925991]
     [-0.37925991  0.          0.16253996  0.        ]
     [ 0.          0.37925991  0.          0.16253996]]

Qualtran integration 🔗

  • It's now possible to convert PennyLane circuits and operators to Qualtran circuits and Bloqs with the new qml.to_bloq function. This function translates PennyLane circuits (qfuncs or QNodes) and operations into equivalent Qualtran bloqs, enabling a new way to estimate the resource requirements of PennyLane quantum circuits via Qualtran's abstractions and tools. (#7197) (#7604) (#7536) (#7814)

    qml.to_bloq can be used in the following ways:

    • Wrap PennyLane circuits and operations to give them Qualtran features, like obtaining bloq_counts and drawing a call_graph, but preserve PennyLane's definition of the circuit/operator. This is done by setting map_ops to False, which instead wraps operations as a ToBloq:

      >>> def circuit():
      ...     qml.X(0)
      ...     qml.Y(1)
      ...     qml.Z(2)
      ...
      >>> cbloq = qml.to_bloq(circuit, map_ops=False)
      >>> type(cbloq)
      pennylane.io.qualtran_io.ToBloq
      >>> cbloq.bloq_counts()
      {XGate(): 1, ZGate(): 1, YGate(): 1}
    • Use smart default mapping of PennyLane circuits and operations to Qualtran Bloqs by setting map_ops=True (the default value):

      >>> PL_op = qml.X(0)
      >>> qualtran_op = qml.to_bloq(PL_op)
      >>> type(qualtran_op)
      qualtran.bloqs.basic_gates.x_basis.XGate
    • Use cus...

Read more

Release 0.41.1

02 May 20:01
db79c1b

Choose a tag to compare

Bug fixes 🐛

  • A warning is raised if PennyLane is imported and a version of JAX greater than 0.4.28 is installed.
    (#7369)

Contributors ✍️

This release contains contributions from (in alphabetical order):

Pietropaolo Frisoni

Release 0.41.0

15 Apr 19:02
bf465ba

Choose a tag to compare

New features since last release

Resource-efficient Decompositions 🔎

A new, experimental graph-based decomposition system is now available in PennyLane, offering more resource-efficiency and versatility. (#6950) (#6951) (#6952) (#7028) (#7045) (#7058) (#7064) (#7149) (#7184) (#7223) (#7263)

PennyLane's new experimental graph decomposition system offers a resource-efficient and versatile alternative to the current system. This is done by traversing an internal graph structure that is weighted by the resources (e.g., gate counts) required to decompose down to a given set of gates.

The graph-based system is experimental and is disabled by default, but it can be enabled by adding qml.decomposition.enable_graph() to the top of your program. Conversely, qml.decomposition.disable_graph() disables the new system from being active.

With qml.decomposition.enable_graph(), the following new features are available:

  • Operators in PennyLane can now accommodate multiple decompositions, which can be queried with the new qml.list_decomps function:

    >>> import pennylane as qml
    >>> qml.decomposition.enable_graph()
    >>> qml.list_decomps(qml.CRX)
    [<pennylane.decomposition.decomposition_rule.DecompositionRule at 0x136da9de0>,
      <pennylane.decomposition.decomposition_rule.DecompositionRule at 0x136da9db0>,
      <pennylane.decomposition.decomposition_rule.DecompositionRule at 0x136da9f00>]
    >>> decomp_rule = qml.list_decomps(qml.CRX)[0]
    >>> print(decomp_rule, "\n")
    @register_resources(_crx_to_rx_cz_resources)
    def _crx_to_rx_cz(phi: TensorLike, wires: WiresLike, **__):
        qml.RX(phi / 2, wires=wires[1])
        qml.CZ(wires=wires)
        qml.RX(-phi / 2, wires=wires[1])
        qml.CZ(wires=wires) 
    
    >>> print(qml.draw(decomp_rule)(0.5, wires=[0, 1]))
    0: ───────────╭●────────────╭●─┤
    1: ──RX(0.25)─╰Z──RX(-0.25)─╰Z─┤

    When an operator within a circuit needs to be decomposed (e.g., when qml.transforms.decompose is present), the chosen decomposition rule is that which is most resource efficient (results in the smallest gate count).

  • New decomposition rules can be globally added to operators in PennyLane with the new qml.add_decomps function. Creating a valid decomposition rule requires:

    • Defining a quantum function that represents the decomposition.
    • Adding resource requirements (gate counts) to the above quantum function by decorating it with the new qml.register_resources function, which requires a dictionary mapping operator types present in the quantum function to their number of occurrences.
    qml.decomposition.enable_graph()
    
    @qml.register_resources({qml.H: 2, qml.CZ: 1})
    def my_cnot(wires):
        qml.H(wires=wires[1])
        qml.CZ(wires=wires)
        qml.H(wires=wires[1])
    
    qml.add_decomps(qml.CNOT, my_cnot)

    This newly added rule for qml.CNOT can be verified as being available to use:

    >>> my_new_rule = qml.list_decomps(qml.CNOT)[-1]
    >>> print(my_new_rule)
    @qml.register_resources({qml.H: 2, qml.CZ: 1})
    def my_cnot(wires):
        qml.H(wires=wires[1])
        qml.CZ(wires=wires)
        qml.H(wires=wires[1])

    Operators with dynamic resource requirements must be declared in a resource estimate using the new qml.resource_rep function. For each operator class, the set of parameters that affects the type of gates and their number of occurrences in its decompositions is given by the resource_keys attribute.

    >>> qml.MultiRZ.resource_keys
    {'num_wires'}

    The output of resource_keys indicates that custom decompositions for the operator should be registered to a resource function (as opposed to a static dictionary) that accepts those exact arguments and returns a dictionary. Consider this dummy example of a fictitious decomposition rule comprising three qml.MultiRZ gates:

    qml.decomposition.enable_graph()
    
    def resource_fn(num_wires):
        return {
            qml.resource_rep(qml.MultiRZ, num_wires=num_wires - 1): 1,
            qml.resource_rep(qml.MultiRZ, num_wires=3): 2
        }
    
    @qml.register_resources(resource_fn)
    def my_decomp(theta, wires):
        qml.MultiRZ(theta, wires=wires[:3])
        qml.MultiRZ(theta, wires=wires[1:])
        qml.MultiRZ(theta, wires=wires[:3])

    More information for defining complex decomposition rules can be found in the documentation for qml.register_resources.

  • The qml.transforms.decompose transform works when the new decompositions system is enabled and offers the ability to inject new decomposition rules for operators in QNodes. (#6966)

    With the graph-based system enabled, the qml.transforms.decompose transform offers the ability to inject new decomposition rules via two new keyword arguments:

    • fixed_decomps: decomposition rules provided to this keyword argument will be used by the new system, bypassing all other decomposition rules that may exist for the relevant operators.
    • alt_decomps: decomposition rules provided to this keyword argument are alternative decomposition rules that the new system may choose if they're the most resource efficient.

    Each keyword argument must be assigned a dictionary that maps operator types to decomposition rules. Here is an example of both keyword arguments in use:

    qml.decomposition.enable_graph()
    
    @qml.register_resources({qml.CNOT: 2, qml.RX: 1})
    def my_isingxx(phi, wires, **__):
        qml.CNOT(wires=wires)
        qml.RX(phi, wires=[wires[0]])
        qml.CNOT(wires=wires)
    
    @qml.register_resources({qml.H: 2, qml.CZ: 1})
    def my_cnot(wires, **__):
        qml.H(wires=wires[1])
        qml.CZ(wires=wires)
        qml.H(wires=wires[1])
    
    @partial(
        qml.transforms.decompose,
        gate_set={"RX", "RZ", "CZ", "GlobalPhase"},
        alt_decomps={qml.CNOT: my_cnot},
        fixed_decomps={qml.IsingXX: my_isingxx},
    )
    @qml.qnode(qml.device("default.qubit"))
    def circuit():
        qml.CNOT(wires=[0, 1])
        qml.IsingXX(0.5, wires=[0, 1])
        return qml.state()
    >>> circuit()
    array([ 9.68912422e-01+2.66934210e-16j, -1.57009246e-16+3.14018492e-16j,
          8.83177008e-17-2.94392336e-17j,  5.44955495e-18-2.47403959e-01j])

    More details about what fixed_decomps and alt_decomps do can be found in the usage details section in the qml.transforms.decompose documentation.

Capturing and Representing Hybrid Programs 📥

Quantum operations, classical processing, structured control flow, and dynamicism can be efficiently expressed with program capture (enabled with qml.capture.enable).

In the last several releases of PennyLane, we have been working on a new and experimental feature called
program capture, which allows for compactly expressing details about hybrid workflows while also providing a smoother integration with just-in-time compilation frameworks like Catalyst (via the ~.pennylane.qjit decorator) and JAX-jit.

Internally, program capture is supported by representing hybrid programs via a new intermediate representation (IR) called plxpr, rather than a quantum tape. The plxpr IR is an adaptation of JAX's jaxpr IR.

There are some quirks and restrictions to be aware of, which are outlined in the :doc:/news/program_capture_sharp_bits page. But with this release, many of the core features of PennyLane—and more!—are available with program capture enabled by adding qml.capture.enable() to the top of your program:

  • QNodes can now contain mid-circuit measurements (MCMs) and classical processing on MCMs with mcm_method = "deferred" when program capture is enabled. (#6838) (#6937) (#6961)

    With mcm_method = "deferred", workflows with mid-circuit measurements can be executed with program capture enabled. Additionally, program capture unlocks the ability to classically process MCM values and use MCM values as gate parameters.

    import jax 
    import jax.numpy as jnp
    jax.config.update("jax_enable_x64", True)
    
    qml.capture.enable()
    
    @qml.qnode(qml.device("default.qubit", wires=3), mcm_method="deferred")
    def f(x):
        m0 = qml.measure(0)
        m1 = qml.measure(0)
    
        # classical processing on m0 and m1
        a = jnp.sin(0.5 * jnp.pi * m0)
        phi = a - (m1 + 1) ** 4
    
        qml.s_prod(x, qml.RX(phi, 0))
    
        return qml.expval(qml.Z(0))
    >>> f(0.1)
    Array(0.00540302, dtype=float64)

    Note that with capture enabled, automatic qubit management is not supported on devices; the number of wires given to the device must coincide with how many MCMs are present in the circuit, since deferred measurements add one auxiliary qubit per MCM.

  • Quantum circuits can now be differentiated on default.qubit and lightning.qubit with diff_method="finite-diff", "adjoint", and "backprop" when program capture is enabled. [(#6853)](https://github.com/PennyLaneAI/pennylane/pul...

Read more

Release 0.40.0

14 Jan 20:40
19ac94b

Choose a tag to compare

New features since last release

Efficient state preparation methods 🦾

  • State preparation tailored for matrix product states (MPS) is now supported with :class:qml.MPSPrep <pennylane.MPSPrep> on the lightning.tensor device. (#6431)

    Given a list of $n$ tensors that represents an MPS, $[A^{(0)}, ..., A^{(n-1)}]$, :class:qml.MPSPrep <pennylane.MPSPrep> lets you directly inject the MPS into a QNode as the initial state of the circuit without any need for pre-processing. The first and last tensors in the list must be rank-2, while all intermediate tensors should be rank-3.

    import pennylane as qml
    import numpy as np
    
    mps = [
        np.array([[0.0, 0.107], [0.994, 0.0]]),
        np.array(
            [
                [[0.0, 0.0, 0.0, -0.0], [1.0, 0.0, 0.0, -0.0]],
                [[0.0, 1.0, 0.0, -0.0], [0.0, 0.0, 0.0, -0.0]],
            ]
        ),
        np.array(
            [
                [[-1.0, 0.0], [0.0, 0.0]],
                [[0.0, 0.0], [0.0, 1.0]],
                [[0.0, -1.0], [0.0, 0.0]],
                [[0.0, 0.0], [1.0, 0.0]],
            ]
        ),
        np.array([[-1.0, -0.0], [-0.0, -1.0]]),
    ]
    
    dev = qml.device("lightning.tensor", wires = [0, 1, 2, 3])
    @qml.qnode(dev)
    def circuit():
        qml.MPSPrep(mps, wires = [0,1,2])
        return qml.state()
    >>> print(circuit())
    [ 0.    +0.j  0.    +0.j  0.    +0.j -0.1066+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.9943+0.j  0.    +0.j  0.    +0.j  0.    +0.j]

    At this time, :class:qml.MPSPrep <pennylane.MPSPrep> is only supported on the lightning.tensor device.

  • Custom-made state preparation for linear combinations of quantum states is now available with :class:qml.Superposition <pennylane.Superposition>. (#6670)

    Given a list of $m$ coefficients $c_i$ and basic states $|b_i\rangle$, :class:qml.Superposition <pennylane.Superposition> prepares $|\phi\rangle = \sum_i^m c_i |b_i\rangle$. Here is a simple example showing how to use :class:qml.Superposition <pennylane.Superposition> to prepare $\tfrac{1}{\sqrt{2}} |00\rangle + \tfrac{1}{\sqrt{2}} |10\rangle$.

    coeffs = np.array([0.70710678, 0.70710678])
    basis =  np.array([[0, 0], [1, 0]])
    
    @qml.qnode(qml.device('default.qubit'))
    def circuit():
        qml.Superposition(coeffs, basis, wires=[0, 1], work_wire=[2])
        return qml.state()
    >>> circuit()
    Array([0.7071068 +0.j, 0.        +0.j, 0.        +0.j, 0.        +0.j,
           0.70710677+0.j, 0.        +0.j, 0.        +0.j, 0.        +0.j],      dtype=complex64)
    

    Note that specification of one work_wire is required.

Enhanced QSVT functionality 🤩

  • New functionality to calculate and convert phase angles for QSP and QSVT has been added with :func:qml.poly_to_angles <pennylane.poly_to_angles> and :func:qml.transform_angles <pennylane.transform_angles>. (#6483)

    The :func:qml.poly_to_angles <pennylane.poly_to_angles> function calculates phase angles directly given polynomial coefficients and the routine in which the angles will be used ("QSVT", "QSP", or "GQSP"):

    >>> poly = [0, 1.0, 0, -1/2, 0, 1/3]
    >>> qsvt_angles = qml.poly_to_angles(poly, "QSVT")
    >>> print(qsvt_angles)
    [-5.49778714  1.57079633  1.57079633  0.5833829   1.61095884  0.74753829]

    The :func:qml.transform_angles <pennylane.transform_angles> function can be used to convert angles from one routine to another:

    >>> qsp_angles = np.array([0.2, 0.3, 0.5])
    >>> qsvt_angles = qml.transform_angles(qsp_angles, "QSP", "QSVT")
    >>> print(qsvt_angles)
    [-6.86858347  1.87079633 -0.28539816]
  • The :func:qml.qsvt <pennylane.qsvt> function has been improved to be more user-friendly, with enhanced capabilities. (#6520) (#6693)

    Block encoding and phase angle computation are now handled automatically, given a matrix to encode, polynomial coefficients, and a block encoding method ("prepselprep", "qubitization", "embedding", or "fable", all implemented with their corresponding operators in PennyLane).

    # P(x) = -x + 0.5 x^3 + 0.5 x^5
    poly = np.array([0, -1, 0, 0.5, 0, 0.5])
    hamiltonian = qml.dot([0.3, 0.7], [qml.Z(1), qml.X(1) @ qml.Z(2)])
    
    dev = qml.device("default.qubit")
    @qml.qnode(dev)
    def circuit():
        qml.qsvt(hamiltonian, poly, encoding_wires=[0], block_encoding="prepselprep")
        return qml.state()
    
    matrix = qml.matrix(circuit, wire_order=[0, 1, 2])()
    >>> print(matrix[:4, :4].real)
    [[-0.1625  0.     -0.3793  0.    ]
     [ 0.     -0.1625  0.      0.3793]
     [-0.3793  0.      0.1625  0.    ]
     [ 0.      0.3793  0.      0.1625]]

    The old functionality can still be accessed with :func:qml.qsvt_legacy <pennylane.qsvt_legacy>.

  • A new :class:qml.GQSP <pennylane.GQSP> template has been added to perform Generalized Quantum Signal Processing (GQSP). (#6565) Similar to QSVT, GQSP is an algorithm that polynomially transforms an input unitary operator, but with fewer restrictions on the chosen polynomial.

    You can also use :func:qml.poly_to_angles <pennylane.poly_to_angles> to obtain angles for GQSP!

    # P(x) = 0.1 + 0.2j x + 0.3 x^2
    poly = [0.1, 0.2j, 0.3]
    angles = qml.poly_to_angles(poly, "GQSP")
    
    @qml.prod # transforms the qfunc into an Operator
    def unitary(wires):
        qml.RX(0.3, wires)
    
    dev = qml.device("default.qubit")
    @qml.qnode(dev)
    def circuit(angles):
        qml.GQSP(unitary(wires = 1), angles, control = 0)
        return qml.state()
    
    matrix = qml.matrix(circuit, wire_order=[0, 1])(angles)
    ``` ```pycon
    >>> print(np.round(matrix,3)[:2, :2])
    [[0.387+0.198j 0.03 -0.089j]
    [0.03 -0.089j 0.387+0.198j]]

Generalized Trotter products 🐖

  • Trotter products that work on exponentiated operators directly instead of full system hamiltonians can now be encoded into circuits with the addition of :class:qml.TrotterizedQfunc <pennylane.TrotterizedQfunc> and :func:qml.trotterize <pennylane.trotterize>. This allows for custom specification of the first-order expansion of the Suzuki-Trotter product formula and extrapolating it to the $n^{\text{th}}$ order. (#6627)

    If the first-order of the Suzuki-Trotter product formula for a given problem is known, :class:qml.TrotterizedQfunc <pennylane.TrotterizedQfunc> and :func:qml.trotterize <pennylane.trotterize> let you implement the $n^{\text{th}}$-order product formula while only specifying the first-order term as a quantum function.

    def my_custom_first_order_expansion(time, theta, phi, wires, flip):
      qml.RX(time * theta, wires[0])
      qml.RY(time * phi, wires[1])
      if flip:
          qml.CNOT(wires=wires[:2])

    :func:qml.trotterize <pennylane.trotterize> requires the quantum function representing the first-order product formula, the number of Trotter steps, and the desired order. It returns a function with the same call signature as the first-order product formula quantum function:

    @qml.qnode(qml.device("default.qubit"))
    def my_circuit(time, theta, phi, num_trotter_steps):
        qml.trotterize(
            first_order_expansion,
            n=num_trotter_steps,
            order=2,
        )(time, theta, phi, wires=['a', 'b'], flip=True)
        return qml.state()

    Alternatively, :class:qml.TrotterizedQfunc <pennylane.TrotterizedQfunc> can be used as follows:

    @qml.qnode(qml.device("default.qubit"))
    def my_circuit(time, theta, phi, num_trotter_steps):
        qml.TrotterizedQfunc(
            time,
            theta,
            phi,
            qfunc=my_custom_first_order_expansion,
            n=num_trotter_steps,
            order=2,
            wires=['a', 'b'],
            flip=True,
        )
        return qml.state()
    >>> time = 0.1
    >>> theta, phi = (0.12, -3.45)
    >>> print(qml.draw(my_circuit, level="device")(time, theta, phi, num_trotter_steps=1))
    a: ──RX(0.01)──╭●─╭●──RX(0.01)──┤  State
    b: ──RY(-0.17)─╰X─╰X──RY(-0.17)─┤  State

    Both methods produce the same results, but offer different interfaces based on the application or overall preference.

Bosonic operators 🎈

A new module, :mod:qml.bose <pennylane.bose>, has been added to PennyLane that includes support for constructing and manipulating Bosonic operators and converting between Bosonic operators and qubit operators.

  • Bosonic operators analogous to qml.FermiWord and qml.FermiSentence are now available with :class:qml.BoseWord <pennylane.BoseWord> and :class:qml.BoseSentence <pennylane.BoseSentence>. (#6518)

    :class:qml.BoseWord <pennylane.BoseWord> and :class:qml.BoseSentence <pennylane.BoseSentence> operate similarly to their fermionic counterparts. To create a Bose word, a dictionary is required as input, where the keys are tuples of boson indices and values are '+/-' (denoting the bosonic creation/annihilation operators). For example, the $b^{\dagger}_0 b_1$ can be constructed as follows.

    >>> w = qml.BoseWord({(0, 0) : '+', (1, 1) : '-'})
    >>> print(w)
    b⁺(0) b(1)

    Multiple Bose words can then be combined to form a Bose sentence:

    >>> w1 = qml.BoseWord({(0, 0) : '+', (1, 1) : '-'})
    >>> w2 = qml.BoseWo...
Read more

Release 0.39.0

05 Nov 19:09
51797f7

Choose a tag to compare

New features since last release

Creating spin Hamiltonians on lattices 💞

  • Functionality for creating custom Hamiltonians on arbitrary lattices has been added. (#6226) (#6237)

    Hamiltonians beyond the available boiler-plate ones in the qml.spin module can be created with the addition of three new functions:

    • qml.spin.Lattice: a new object for instantiating customized lattices via primitive translation vectors and unit cell parameters,
    • qml.spin.generate_lattice: a utility function for creating standard Lattice objects, including 'chain', 'square', 'rectangle', 'triangle', 'honeycomb', 'kagome', 'lieb', 'cubic', 'bcc', 'fcc', and 'diamond',
    • qml.spin.spin_hamiltonian: generates a spin Hamiltonian object given a Lattice object with custom edges/nodes.

    An example is shown below for a $3 \times 3$ triangular lattice with open boundary conditions.

    lattice = qml.spin.Lattice(
        n_cells=[3, 3],
        vectors=[[1, 0], [np.cos(np.pi/3), np.sin(np.pi/3)]],
        positions=[[0, 0]],
        boundary_condition=False
    )

    We can validate this lattice against qml.spin.generate_lattice('triangle', ...) by checking the lattice_points (the $(x, y)$ coordinates of all sites in the lattice):

    >>> lp = lattice.lattice_points
    >>> triangular_lattice = qml.spin.generate_lattice('triangle', n_cells=[3, 3])
    >>> np.allclose(lp, triangular_lattice.lattice_points)
    True

    The edges of the Lattice object are nearest-neighbour by default, where we can add edges by using its add_edge method.

    Optionally, a Lattice object can have interactions and fields endowed to it by specifying values for its custom_edges and custom_nodes keyword arguments. The Hamiltonian can then be extracted with the qml.spin.spin_hamiltonian function. An example is shown below for the transverse-field Ising model Hamiltonian on a $3 \times 3$ triangular lattice. Note that the custom_edges and custom_nodes keyword arguments only need to be defined for one unit cell repetition.

    edges = [
        (0, 1), (0, 3), (1, 3)
    ]
    
    lattice = qml.spin.Lattice(
        n_cells=[3, 3],
        vectors=[[1, 0], [np.cos(np.pi/3), np.sin(np.pi/3)]],
        positions=[[0, 0]],
        boundary_condition=False,
        custom_edges=[[edge, ("ZZ", -1.0)] for edge in edges], 
        custom_nodes=[[i, ("X", -0.5)] for i in range(3*3)],
    )
    >>> tfim_ham = qml.spin.transverse_ising('triangle', [3, 3], coupling=1.0, h=0.5)
    >>> tfim_ham == qml.spin.spin_hamiltonian(lattice=lattice)
    True
  • More industry-standard spin Hamiltonians have been added in the qml.spin module. (#6174) (#6201)

    Three new industry-standard spin Hamiltonians are now available with PennyLane v0.39:

    These additions accompany qml.spin.heisenberg, qml.spin.transverse_ising, and qml.spin.fermi_hubbard, which were introduced in v0.38.

Calculating Polynomials 🔢

  • Polynomial functions can now be easily encoded into quantum circuits with qml.OutPoly. (#6320)

    A new template called qml.OutPoly is available, which provides the ability to encode a polynomial function in a quantum circuit. Given a polynomial function $f(x_1, x_2, \cdots, x_N)$, qml.OutPoly requires:

    • f : a standard Python function that represents $f(x_1, x_2, \cdots, x_N)$,
    • input_registers ($\vert x_1 \rangle$, $\vert x_2 \rangle$, ..., $\vert x_N \rangle$) : a list/tuple containing Wires objects that correspond to the embedded numeric values of $x_1, x_2, \cdots, x_N$,
    • output_wires : the Wires for which the numeric value of $f(x_1, x_2, \cdots, x_N)$ is stored.

    Here is an example of using qml.OutPoly to calculate $f(x_1, x_2) = 3x_1^2 - x_1x_2$ for $f(1, 2) = 1$.

    wires = qml.registers({"x1": 1, "x2": 2, "output": 2})
    
    def f(x1, x2):
        return 3 * x1 ** 2 - x1 * x2
    
    @qml.qnode(qml.device("default.qubit", shots = 1))
    def circuit():
        # load values of x1 and x2
        qml.BasisEmbedding(1, wires=wires["x1"])
        qml.BasisEmbedding(2, wires=wires["x2"])
    
        # apply the polynomial
        qml.OutPoly(
            f,
            input_registers = [wires["x1"], wires["x2"]],
            output_wires = wires["output"])
    
        return qml.sample(wires=wires["output"])
    >>> circuit()
    array([0, 1])

    The result, [0, 1], is the binary representation of $1$. By default, the result is calculated modulo $2^\text{len(output wires)}$ but can be overridden with the mod keyword argument.

Readout Noise 📠

  • Readout errors can now be included in qml.NoiseModel and qml.add_noise with the new qml.noise.meas_eq function. (#6321)

    Measurement/readout errors can be specified in a similar fashion to regular gate noise in PennyLane: a newly added Boolean function called qml.noise.meas_eq that accepts a measurement function (e.g., qml.expval, qml.sample, or any other function that can be returned from a QNode) that, when present in the QNode, inserts a noisy operation via qml.noise.partial_wires or a custom noise function. Readout noise in PennyLane also follows the insertion convention, where the specified noise is inserted before the measurement.

    Here is an example of adding qml.PhaseFlip noise to any qml.expval measurement:

    c0 = qml.noise.meas_eq(qml.expval)
    n0 = qml.noise.partial_wires(qml.PhaseFlip, 0.2)

    To include this in a qml.NoiseModel, use its meas_map keyword argument:

    # gate-based noise
    c1 = qml.noise.wires_in([0, 2]) 
    n1 = qml.noise.partial_wires(qml.RY, -0.42)
    
    noise_model = qml.NoiseModel({c1: n1}, meas_map={c0: n0})
    >>> noise_model
    NoiseModel({
      WiresIn([0, 2]): RY(phi=-0.42)
    },
    meas_map = {
        MeasEq(expval): PhaseFlip(p=0.2)
    })

    qml.noise.meas_eq can also be combined with other Boolean functions in qml.noise via bitwise operators for more versatility.

    To add this noise_model to a circuit, use the qml.add_noise transform as per usual. For example,

    @qml.qnode(qml.device("default.mixed", wires=3))
    def circuit():
        qml.RX(0.1967, wires=0)
        for i in range(3):
            qml.Hadamard(i)
    
        return qml.expval(qml.X(0) @ qml.X(1))
    >>> noisy_circuit = qml.add_noise(circuit, noise_model)
    >>> print(qml.draw(noisy_circuit)())
    0: ──RX(0.20)──RY(-0.42)────────H──RY(-0.42)──PhaseFlip(0.20)─┤ ╭<X@X>
    1: ──H─────────PhaseFlip(0.20)────────────────────────────────┤ ╰<X@X>
    2: ──H─────────RY(-0.42)──────────────────────────────────────┤    
    >>> print(circuit(), noisy_circuit())
    0.9807168489852615 0.35305806563469433

User-friendly decompositions 📠

  • A new transform called qml.transforms.decompose has been added to better facilitate the custom decomposition of operators in PennyLane circuits. (#6334)

    Previous to the addition of qml.transforms.decompose, decomposing operators in PennyLane had to be done by specifying a stopping_condition in qml.device.preprocess.decompose. With qml.transforms.decompose, the user-interface for specifying decompositions is much simpler and more versatile.

    Decomposing gates in a circuit can be done a few ways:

    • Specifying a gate_set comprising PennyLane Operators to decompose into:

      from functools import partial
      
      dev = qml.device('default.qubit')
      allowed_gates = {qml.Toffoli, qml.RX, qml.RZ}
      
      @partial(qml.transforms.decompose, gate_set=allowed_gates)
      @qml.qnode(dev)
      def circuit():
          qml.Hadamard(wires=[0])
          qml.Toffoli(wires=[0, 1, 2])
          return qml.expval(qml.Z(0))
      >>> print(qml.draw(circuit)())
      0: ──RZ(1.57)──RX(1.57)──RZ(1.57)─╭●─┤  <Z>
      1: ───────────────────────────────├●─┤
      2: ───────────────────────────────╰X─┤
    • Specifying a gate_set that is defined by a rule (Boolean function). For example, one can specify an arbitrary gate set to decompose into, so long as the resulting gates only act on one or two qubits:

      @partial(qml.transforms.decompose, gate_set = lambda op: len(op.wires) <= 2)
      @qml.qnode(dev)
      def circuit():
          qml.Toffoli(wires=[0, 1, 2])
          return qml.expval(qml.Z(0))
      >>> print(qml.draw(circuit)())
      0: ───────────╭●───────────╭●────╭●──T──╭●─┤  <Z>
      1: ────╭●─────│─────╭●─────│───T─╰X──T†─╰X─┤     
      2: ──H─╰X──T†─╰X──T─╰X──T†─╰X──T──H────────┤
    • Specifying a value for max_expansion. By default, decomposition occurs recursively until the desired gate set is reached, but this can be overridden to control the number of passes.

      phase = 1.0
      target_wires = [0]
      unitary = qml.RX(phase, wires=0).matrix()
      n_estimation_wires = 1
      estimation_wires = range(1, n_estimation_wires + 1)
      
      def qfunc():
          qml.QuantumPhaseEstimation(
              unitary,
              target_wires=target_wires,
              est...
Read more

Release 0.38.0

03 Sep 22:39
9ba6205

Choose a tag to compare

New features since last release

Registers of wires 🧸

  • A new function called qml.registers has been added that lets you seamlessly create registers of wires. (#5957) (#6102)

    Using registers, it is easier to build large algorithms and circuits by applying gates and operations to predefined collections of wires. With qml.registers, you can create registers of wires by providing a dictionary whose keys are register names and whose values are the number of wires in each register.

    >>> wire_reg = qml.registers({"alice": 4, "bob": 3})
    >>> wire_reg
    {'alice': Wires([0, 1, 2, 3]), 'bob': Wires([4, 5, 6])}

    The resulting data structure of qml.registers is a dictionary with the same register names as keys, but the values are qml.wires.Wires instances.

    Nesting registers within other registers can be done by providing a nested dictionary, where the ordering of wire labels is based on the order of appearance and nestedness.

    >>> wire_reg = qml.registers({"alice": {"alice1": 1, "alice2": 2}, "bob": {"bob1": 2, "bob2": 1}})
    >>> wire_reg
    {'alice1': Wires([0]), 'alice2': Wires([1, 2]), 'alice': Wires([0, 1, 2]), 'bob1': Wires([3, 4]), 'bob2': Wires([5]), 'bob': Wires([3, 4, 5])}

    Since the values of the dictionary are Wires instances, their use within quantum circuits is very similar to that of a list of integers.

    dev = qml.device("default.qubit")
    
    @qml.qnode(dev)
    def circuit():
        for w in wire_reg["alice"]:
            qml.Hadamard(w)
    
        for w in wire_reg["bob1"]:
            qml.RX(0.1967, wires=w)
    
        qml.CNOT(wires=[wire_reg["alice1"][0], wire_reg["bob2"][0]])
    
        return [qml.expval(qml.Y(w)) for w in wire_reg["bob1"]]
    
    print(qml.draw(circuit)())
    0: ──H────────╭●─┤     
    1: ──H────────│──┤     
    2: ──H────────│──┤     
    3: ──RX(0.20)─│──┤  <Y>
    4: ──RX(0.20)─│──┤  <Y>
    5: ───────────╰X─┤  

    In tandem with qml.registers, we've also made the following improvements to qml.wires.Wires:

    • Wires instances now have a more copy-paste friendly representation when printed. (#5958)

      >>> from pennylane.wires import Wires
      >>> w = Wires([1, 2, 3])
      >>> w
      Wires([1, 2, 3])
    • Python set-based combinations are now supported by Wires. (#5983)

      This new feature unlocks the ability to combine Wires instances in the following ways:

      • intersection with & or intersection():

        >>> wires1 = Wires([1, 2, 3])
        >>> wires2 = Wires([2, 3, 4])
        >>> wires1.intersection(wires2) # or wires1 & wires2
        Wires([2, 3])
      • symmetric difference with ^ or symmetric_difference():

        >>> wires1.symmetric_difference(wires2) # or wires1 ^ wires2
        Wires([1, 4])
      • union with | or union():

        >>> wires1.union(wires2) # or wires1 | wires2
        Wires([1, 2, 3, 4])
      • difference with - or difference():

        >>> wires1.difference(wires2) # or wires1 - wires2
        Wires([1])

Quantum arithmetic operations 🧮

  • Several new operator templates have been added to PennyLane that let you perform quantum arithmetic operations. (#6109) (#6112) (#6121)

    • qml.Adder performs in-place modular addition: $\text{Adder}(k, m)\vert x \rangle = \vert x + k ; \text{mod} ; m\rangle$.

    • qml.PhaseAdder is similar to qml.Adder, but it performs in-place modular addition in the Fourier basis.

    • qml.Multiplier performs in-place multiplication: $\text{Multiplier}(k, m)\vert x \rangle = \vert x \times k ; \text{mod} ; m \rangle$.

    • qml.OutAdder performs out-place modular addition: $\text{OutAdder}(m)\vert x \rangle \vert y \rangle \vert b \rangle = \vert x \rangle \vert y \rangle \vert b + x + y ; \text{mod} ; m \rangle$.

    • qml.OutMultiplier performs out-place modular multiplication: $\text{OutMultiplier}(m)\vert x \rangle \vert y \rangle \vert b \rangle = \vert x \rangle \vert y \rangle \vert b + x \times y ; \text{mod} ; m \rangle$.

    • qml.ModExp performs modular exponentiation: $\text{ModExp}(base, m) \vert x \rangle \vert k \rangle = \vert x \rangle \vert k \times base^x ; \text{mod} ; m \rangle$.

    Here is a comprehensive example that performs the following calculation: (2 + 1) * 3 mod 7 = 2 (or 010 in binary).

    dev = qml.device("default.qubit", shots=1)
    
    wire_reg = qml.registers({
        "x_wires": 2, # |x>: stores the result of 2 + 1 = 3
        "y_wires": 2, # |y>: multiples x by 3
        "output_wires": 3, # stores the result of (2 + 1) * 3 m 7 = 2
        "work_wires": 2 # for qml.OutMultiplier
    })
    
    @qml.qnode(dev)
    def circuit():
        # In-place addition
        qml.BasisEmbedding(2, wires=wire_reg["x_wires"])
        qml.Adder(1, x_wires=wire_reg["x_wires"]) # add 1 to wires [0, 1] 
    
        # Out-place multiplication
        qml.BasisEmbedding(3, wires=wire_reg["y_wires"])
        qml.OutMultiplier(
            wire_reg["x_wires"], 
            wire_reg["y_wires"], 
            wire_reg["output_wires"], 
            work_wires=wire_reg["work_wires"], 
            mod=7
        ) 
    
        return qml.sample(wires=wire_reg["output_wires"])
    >>> circuit()
    array([0, 1, 0])
    

Converting noise models from Qiskit ♻️

  • Convert Qiskit noise models into a PennyLane NoiseModel with qml.from_qiskit_noise. (#5996)

    In the last few releases, we've added substantial improvements and new features to the Pennylane-Qiskit plugin. With this release, a new qml.from_qiskit_noise function allows you to convert a Qiskit noise model into a PennyLane NoiseModel. Here is a simple example with two quantum errors that add two different depolarizing errors based on the presence of different gates in the circuit:

    import pennylane as qml
    import qiskit_aer.noise as noise
    
    error_1 = noise.depolarizing_error(0.001, 1) # 1-qubit noise
    error_2 = noise.depolarizing_error(0.01, 2) # 2-qubit noise
    
    noise_model = noise.NoiseModel()
    
    noise_model.add_all_qubit_quantum_error(error_1, ['rz', 'ry'])
    noise_model.add_all_qubit_quantum_error(error_2, ['cx'])
    >>> qml.from_qiskit_noise(noise_model)
    NoiseModel({
      OpIn(['RZ', 'RY']): QubitChannel(num_kraus=4, num_wires=1)
      OpIn(['CNOT']): QubitChannel(num_kraus=16, num_wires=2)
    })

    Under the hood, PennyLane converts each quantum error in the Qiskit noise model into an equivalent qml.QubitChannel operator with the same canonical Kraus representation. Currently, noise models in PennyLane do not support readout errors. As such, those will be skipped during conversion if they are present in the Qiskit noise model.

    Make sure to pip install pennylane-qiskit to access this new feature!

Substantial upgrades to mid-circuit measurements using tree-traversal 🌳

  • The "tree-traversal" algorithm for mid-circuit measurements (MCMs) on default.qubit has been internally redesigned for better performance. (#5868)

    In the last release (v0.37), we introduced the tree-traversal MCM method, which was implemented in a recursive way for simplicity. However, this had the unintended consequence of very deep stack calls for circuits with many MCMs, resulting in stack overflows in some cases. With this release, we've refactored the implementation of the tree-traversal method into an iterative approach, which solves those inefficiencies when many MCMs are present in a circuit.

  • The tree-traversal algorithm is now compatible with analytic-mode execution (shots=None). (#5868)

    dev = qml.device("default.qubit")
    
    n_qubits = 5
    
    @qml.qnode(dev, mcm_method="tree-traversal")
    def circuit():
        for w in range(n_qubits):
            qml.Hadamard(w)
        
        for w in range(n_qubits - 1):
            qml.CNOT(wires=[w, w+1])
    
        for w in range(n_qubits):
            m = qml.measure(w)
            qml.cond(m == 1, qml.RX)(0.1967 * (w + 1), w)
    
        return [qml.expval(qml.Z(w)) for w in range(n_qubits)]
    >>> circuit()
    [tensor(0.00964158, requires_grad=True),
     tensor(0.03819446, requires_grad=True),
     tensor(0.08455748, requires_grad=True),
     tensor(0.14694258, requires_grad=True),
     tensor(0.2229438, requires_grad=True)]

Improvements 🛠

Creating spin Hamiltonians

  • Three new functions are now available for creating commonly-used spin Hamiltonians in PennyLane: (#6106) (#6128)

Read more