Skip to content

⬆️🐍 Update dependency pennylane to ~=0.45.0#145

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/pennylane-0.x
Open

⬆️🐍 Update dependency pennylane to ~=0.45.0#145
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/pennylane-0.x

Conversation

@renovate

@renovate renovate Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
pennylane ~=0.44.1~=0.45.0 age confidence

Release Notes

PennyLaneAI/pennylane (pennylane)

v0.45.0: Release 0.45.0

Compare Source

New features since last release

Sum of Slaters State Preparation 🆘

  • A new state preparation method called SumOfSlatersPrep is now available. This state preparation routine introduced in Fomichev et al., PRX Quantum 5, 040339 (see the associated demo) is a state-of-the-art technique for algorithms that require a high-quality initial state, like in ground-state energy estimation algorithms of chemical systems. (#​8964) (#​8997) (#​9228) (#​9323)

    Consider this sparse state on three qubits, specified by normalized coefficients and statevector indices pointing to the populated computational basis states:

    import pennylane as qp
    import numpy as np
    
    coefficients = np.array([1, -1j, 1j, 1]) / np.sqrt(4)
    indices = (0, 1, 2, 4)
    num_wires = 3
    wires = list(range(num_wires))

    The SumOfSlatersPrep operation requires the coefficients, indices, and wires as input. It efficiently prepares sparse states using auxiliary wires, QROM\ s, reversible bit encodings, and a dense-state preparation on a smaller subspace. Its implementation can be inspected by using PennyLane's graph-based decomposition algorithm (enable_graph) with the following gate set and five auxiliary wires (work_wires).

    qp.decomposition.enable_graph()
    
    gate_set = {"QROM", "TemporaryAND", "Adjoint(TemporaryAND)", "StatePrep", "CNOT", "X"}
    
    num_work_wires = 5
    
    @​qp.decompose(gate_set=gate_set, num_work_wires=num_work_wires)
    @​qp.qnode(qp.device("lightning.qubit", wires=num_work_wires+num_wires))
    def circuit():
        qp.SumOfSlatersPrep(coefficients, wires, indices)
        return qp.state()
    >>> print(qp.draw(circuit, show_matrices=False, max_length=180)())
                0: ────────────────╭QROM(M0)──X─╭●────────────●╮────╭●────────────●╮──X─╭●───────────────●╮─────────────┤  State
                1: ────────────────├QROM(M0)──X─├●────────────●┤──X─├●────────────●┤──X─├●───────────────●┤──X──────────┤  State
                2: ────────────────├QROM(M0)────│──╭●─────●╮───│──X─│──╭●─────●╮───│────│──╭●────────●╮───│──X──────────┤  State
    <DynamicWire>: ─╭Allocate─╭|Ψ⟩─├QROM(M0)────│──│───────│───│────│──│──╭X───│───│────│──│──╭X──────│───│─╭Deallocate─┤  State
    <DynamicWire>: ─├Allocate─╰|Ψ⟩─├QROM(M0)────│──│──╭X───│───│────│──│──│────│───│────│──│──│──╭X───│───│─├Deallocate─┤  State
    <DynamicWire>: ─├Allocate──────╰QROM(M0)────│──│──│────│───│────│──│──│────│───│────│──│──│──│────│───│─├Deallocate─┤  State
    <DynamicWire>: ─├Allocate───────────────────╰⊕─├●─│───●┤──⊕╯────╰⊕─├●─│───●┤──⊕╯────╰⊕─├●─│──│───●┤──⊕╯─├Deallocate─┤  State
    <DynamicWire>: ─╰Allocate──────────────────────╰⊕─╰●──⊕╯───────────╰⊕─╰●──⊕╯───────────╰⊕─╰●─╰●──⊕╯─────╰Deallocate─┤  State

    The smaller dense state preparation is represented by |Ψ⟩, which is on two wires, and the <DynamicWire> labels represent dynamically allocated wires (with allocate) used in its decomposition.

Workflow Inspection 🔍

  • When using specs with qjit and level="all", you can now display the returned CircuitSpecs as a table, allowing you to easily see how circuit resources evolve with each stage of compilation. In addition, specs now supports setting level="user" for workflows compiled with qjit, returning circuit specifications after all user-specified transforms have been applied. (#​9088) (#​9307) (#​9426)

    @&#8203;qp.qjit
    @&#8203;qp.transforms.merge_rotations
    @&#8203;qp.transforms.cancel_inverses
    @&#8203;qp.qnode(qp.device("lightning.qubit", wires=2))
    def circuit():
        qp.RX(1.23,0)
        qp.RX(1.23,0)
        qp.X(0)
        qp.H(0)
        qp.H(0)
        return qp.probs()
    >>> print(qp.specs(circuit, level="all")())
    Device: lightning.qubit
    Device wires: 2
    Shots: Shots(total=None)
    Levels:
    - 0: Before MLIR Passes
    - 1: cancel-inverses
    - 2: merge-rotations
    <BLANKLINE>
    ↓Metric     Level→ |  0 |  1 |  2
    ---------------------------------
    Wire allocations   |  2 |  2 |  2
    Total gates        |  5 |  3 |  2
    Gate counts:       |
    - Hadamard         |  2 |  0 |  0
    - PauliX           |  1 |  1 |  1
    - RX               |  2 |  2 |  1
    Measurements:      |
    - probs(all wires) |  1 |  1 |  1

    We can also observe the specifications after all user-applied transforms using level="user":

    >>> print(qp.specs(circuit, level="user")())
    Device: lightning.qubit
    Device wires: 2
    Shots: Shots(total=None)
    Level: merge-rotations
    
    Wire allocations: 2
    Total gates: 2
    Gate counts:
    - PauliX: 1
    - RX: 1
    Measurements:
    - probs(all wires): 1
    Depth: Not computed
  • When inspecting a circuit via the level argument in specs or draw, markers placed in a CompilePipeline (with marker) are now accessible exclusively via their label, making it much easier to track levels of compilation without having to track shifting integer level values. In addition, markers can now be added directly to a CompilePipeline with the add_marker method, and printing a CompilePipeline now legibly distinguishes transforms and markers. (#​8990) (#​9007) (#​9076) (#​9102)

    pipeline = qp.CompilePipeline()
    pipeline.add_marker("no-transforms")
    pipeline += qp.transforms.cancel_inverses
    
    @&#8203;qp.marker("after-cancel-inverses")
    @&#8203;pipeline
    @&#8203;qp.qnode(qp.device("default.qubit"))
    def circuit():
      qp.X(0)
      qp.H(0)
      qp.H(0)
      return qp.probs()

    The new string representation of CompilePipeline allows you to inspect the transforms and markers:

    >>> print(circuit.compile_pipeline)
    CompilePipeline(
       ├─▶ no-transforms
      [1] cancel_inverses()
       └─▶ after-cancel-inverses
    )

    As usual, marker labels can be used as an argument to level in specs and draw, showing the cumulative result of compilation up to the provided marker:

    >>> print(qp.draw(circuit, level="no-transforms")()) # or level=0
    0: ──X──H──H─┤  Probs
    >>> print(qp.draw(circuit, level="after-cancel-inverses")()) # or level=1
    0: ──X─┤  Probs
  • specs now includes PPR and PPM weights in its output, allowing for better categorization of PPMs and PPRs in workflows compiled with qjit. (#​8983)

    @&#8203;qp.qjit
    @&#8203;qp.transforms.to_ppr
    @&#8203;qp.qnode(qp.device("null.qubit", wires=2))
    def circuit():
        qp.H(0)
        qp.CNOT([0, 1])
        m = qp.measure(0)
        qp.T(0)
        return qp.expval(qp.Z(0))
    >>> print(qp.specs(circuit, level="user")())
    Device: null.qubit
    Device wires: 2
    Shots: Shots(total=None)
    Level: to-ppr
    <BLANKLINE>
    Wire allocations: 2
    Total gates: 11
    Gate counts:
    - GlobalPhase: 3
    - PPM-w1: 1
    - PPR-pi/4-w1: 5
    - PPR-pi/4-w2: 1
    - PPR-pi/8-w1: 1
    Measurements:
    - expval(PauliZ): 1
    Depth: Not computed
    
  • specs has been upgraded with significantly faster processing of large workflows with many gates and/or measurements for qjit compiled workflows in pass-by-pass mode. This is achieved using Catalyst's ResourceAnalysis pass behind the scenes, improving upon the former implementation. For more details, check out the Catalyst v0.15 release notes. (#​9279)

  • specs now returns measurement information for qjit workloads when using level="device". (#​8988)

  • When using pass-by-pass specs with Catalyst, the output will no longer display a "Before Tape Transforms" level if no tape transforms have been applied. In particular, for scenarios where no tape transforms are present, the "Before MLIR passes" level becomes level 0. In scenarios with at least one tape transform, level 0 corresponds to "Before Tape Transforms" and "Before MLIR passes" is the level after all tape transforms but before the first MLIR pass. (#​9091) (#​9166)

QSVT Angle Solver 📐

  • A new angle solver has been added to find QSVT phase angles faster for large-degree polynomials. This can be accessed by setting angle_solver="iterative-optax" in qsvt and poly_to_angles, where the benefits are seen when when repeatedly evaluating the same-degree polynomial with different coefficients. Note that this requires optax to be installed. (#​8685) (#​9435)

    poly = np.array([0, 1.0, 0, -1/2, 0, 1/3])
    qsvt_angles = qp.poly_to_angles(poly, "QSVT", angle_solver="iterative-optax")
    >>> print(qsvt_angles)
    [-4.74724627  1.51868559  0.57952342  0.57952342  1.51868559 -0.03485729]

Decomposition Inspection and Pre-defined Gate Sets 📠

New tools dedicated to accessible inspectability of PennyLane's graph-based decomposition system (enabled with enable_graph) are now available! With this release, you can query the solutions of the graph-based system to understand how PennyLane decomposed a circuit, why specific rules where chosen over others, and more.

  • It is now possible to assign custom names to decomposition rules using the name argument in qp.register_resources, making it easier to identify specific decomposition rules. (#​9257)

    import pennylane as qp
    
    qp.decomposition.enable_graph()
    
    @&#8203;qp.register_resources({qp.CNOT: 1, qp.H: 2}, name='my_cz_rule')
    def cz_to_h_cnot(wires):
        qp.H(wires[1])
        qp.CNOT(wires)
        qp.H(wires[1])
    ``` ```pycon
    >>> cz_to_h_cnot.name
    'my_cz_rule'
  • A new function called inspect_decomps allows for the visualization and inspection of all possible decomposition paths the graph system can take for a concrete operator instance. (#​9322) (#​9359) (#​9427)

    For each decomposition rule applicable to the operator instance, the output includes its name, circuit diagram, gate count, and wire allocation (if any):

    >>> qp.inspect_decomps(qp.CRX(0.5, wires=[0, 1]))
    Decomposition 0 (name: _crx_to_rx_cz)
    0: ───────────╭●────────────╭●─┤
    1: ──RX(0.25)─╰Z──RX(-0.25)─╰Z─┤
    Gate Count: {RX: 2, CZ: 2}
    
    Decomposition 1 (name: _crx_to_rz_ry)
    0: ─────────────────────╭●────────────╭●────────────┤
    1: ──RZ(1.57)──RY(0.25)─╰X──RY(-0.25)─╰X──RZ(-1.57)─┤
    Gate Count: {RZ: 2, RY: 2, CNOT: 2}
    
    Decomposition 2 (name: _crx_to_h_crz)
    0: ────╭●───────────┤
    1: ──H─╰RZ(0.50)──H─┤
    Gate Count: {Hadamard: 2, CRZ: 1}
    
    Decomposition 3 (name: _crx_to_ppr)
    0: ───────────╭RZX(-0.25)─┤
    1: ──RX(0.25)─╰RZX(-0.25)─┤
    Gate Count: {PauliRot(pauli_word=ZX): 1, PauliRot(pauli_word=X): 1}

    By default, inspect_decomps displays all available decomposition rules for an operator. Alternatively, a single decomposition rule can be inspected by passing its name:

    >>> qp.inspect_decomps(qp.CRX(0.5, wires=[0, 1]), "_crx_to_h_crz")
    Decomposition 0 (name: _crx_to_h_crz)
    0: ────╭●───────────┤
    1: ──H─╰RZ(0.50)──H─┤
    Gate Count: {Hadamard: 2, CRZ: 1}
  • A new function called decomp_inspector is available for verifying how the decomposition graph chooses decomposition rules for each operator instance in a circuit. (#​9359) (#​9436)

    The decomp_inspector acts as a transform that can be applied on a QNode as a decorator. It returns an object that allows for interactively querying a given operator to identify which decomposition rules were considered and which one was chosen.

    Consider the following example where we want to efficiently decompose a MultiRZ into single-qubit rotations and CNOTs:

    qp.decomposition.enable_graph()
    
    gate_sets = {"CNOT", "RX", "RY", "RZ", "Identity", "GlobalPhase", "MidMeasureMP"}
    
    @&#8203;qp.decomp_inspector(gate_set=gate_sets, num_work_wires=2)
    @&#8203;qp.qnode(qp.device("default.qubit"))
    def circuit():
        qp.ctrl(qp.MultiRZ(0.5, [0, 1]), control=[3, 4, 5])
        return qp.probs()
    
    inspector = circuit()

    We can then call the inspector's inspect_decomps method and provide the MultiRZ instance of interest to see which decomposition rules were considered.

    >>> inspector.inspect_decomps(qp.ctrl(qp.MultiRZ(0.5, [0, 1]), control=[3, 4, 5]), num_work_wires=2)
    CHOSEN: Decomposition 0 (name: flip_zero_ctrl_values(_ctrl_single_work_wire))
    <DynamicWire>: ──Allocate─╭X─╭●─────────────╭X──Deallocate─┤
                3: ───────────├●─│──────────────├●─────────────┤
                4: ───────────├●─│──────────────├●─────────────┤
                5: ───────────╰●─│──────────────╰●─────────────┤
                0: ──────────────├MultiRZ(0.50)────────────────┤
                1: ──────────────╰MultiRZ(0.50)────────────────┤
    First-Level Expansion Gates: {MultiControlledX(num_control_wires=3, num_work_wires=0, num_zero_control_values=0, work_wire_type=borrowed): 2, Controlled(MultiRZ(num_wires=2), num_control_wires=1, num_work_wires=0, num_zero_control_values=0, work_wire_type=borrowed): 1}
    Wire Allocations: {'zero': 1}
    Full Expansion Gates: {RZ: 58, CNOT: 34, GlobalPhase: 64, RY: 18, RX: 8, MidMeasure: 2}
    Weighted Cost: 120.0
    
    Decomposition 1 (name: to_controlled_qubit_unitary)
    Not applicable (provided operator instance does not meet all conditions for this rule).
    
    Decomposition 2 (name: controlled(_multi_rz_decomposition))
    0: ─╭X─╭RZ(0.50)─╭X─┤
    1: ─├●─│─────────├●─┤
    3: ─├●─├●────────├●─┤
    4: ─├●─├●────────├●─┤
    5: ─╰●─╰●────────╰●─┤
    First-Level Expansion Gates: {Controlled(RZ, num_control_wires=3, num_work_wires=0, num_zero_control_values=0, work_wire_type=borrowed): 1, MultiControlledX(num_control_wires=4, num_work_wires=0, num_zero_control_values=0, work_wire_type=borrowed): 2}
    Full Expansion Gates: {GlobalPhase: 76, RX: 16, MidMeasure: 4, RY: 24, RZ: 80, CNOT: 72}
    Weighted Cost: 196.0

    For each decomposition rule applicable to the controlled MultiRZ operator instance, the inspector provides a summary of its weighted cost, wire allocations, and the "Full Expansion" (the final gate counts produced after decomposing all the way down to the target gate set).

    Similar to the qp.decompose transform, the decomp_inspector provides the ability to inject new decomposition rules via the keyword arguments fixed_decomps and alt_decomps. For more details on the inspection capabilities please consult the documentation for decomp_inspector.

  • The list_decomps function now returns an object that is easier to interact with, including better legibility when printing the entire set of available decomposition rules and when printing individual ones. Additionally, the object returned supports accessing a specific rule by index or by name. (#​9260)

    >>> collection = qp.list_decomps(qp.CRX)
    >>> print(collection)
    Available Decomposition Rules:
    0: _crx_to_rx_cz
    1: _crx_to_rz_ry
    2: _crx_to_h_crz
    3: _crx_to_ppr
    >>> collection[0]
    DecompositionRule(name=_crx_to_rx_cz)
    >>> collection['_crx_to_ppr']
    DecompositionRule(name=_crx_to_ppr)
    >>> print(qp.draw(collection[0])(0.5, wires=[0, 1]))
    0: ───────────╭●────────────╭●─┤
    1: ──RX(0.25)─╰Z──RX(-0.25)─╰Z─┤
  • A new gate_sets module contains pre-defined gate sets that can be plugged into the gate_set argument of the decompose transform. These pre-defined gate sets can be easily accessed and integrated into decompositions workflows. Key gate sets include: (#​8915) (#​9045) (#​9259) (#​9417)

    • qp.gate_sets.CLIFFORD_T which contains the Clifford+T gate set and qp.gate_sets.CLIFFORD_T_PLUS_RZ with an additional RZ gate. - qp.gate_sets.ROTATIONS_PLUS_CNOT which contains single-qubit rotations and CNOT. - qp.gate_sets.IDENTITY which contains the Identity and the GlobalPhase gates.

    Here is an example using the ROTATIONS_PLUS_CNOT gate set to decompose a controlled MultiRZ gate:

    qp.decomposition.enable_graph()
    
    @&#8203;qp.decompose(gate_set=qp.gate_sets.ROTATIONS_PLUS_CNOT, num_work_wires=2)
    @&#8203;qp.qnode(qp.device("default.qubit"))
    def circuit():
        qp.ctrl(qp.MultiRZ(0.5, [0, 1]), control=[3, 4, 5])
        return qp.expval(qp.Z(0))
    >>> print(qp.specs(circuit, level="device")().resources.gate_counts)
    {'RZ': 54, 'RY': 14, 'GlobalPhase': 52, 'CNOT': 36, 'CRZ': 4, 'CRY': 4, 'C(GlobalPhase)': 4, 'Toffoli': 2}

Resource Estimation Templates 📏

  • New lightweight representations of the HybridQRAM, SelectOnlyQRAM, BasisEmbedding, and BasisState templates have been added for fast and efficient resource estimation. These are available in the estimator module as: qp.estimator.HybridQRAM, qp.estimator.SelectOnlyQRAM, qp.estimator.BasisEmbedding, and qp.estimator.BasisState. (#​8828) (#​8826) (#​9415) (#​9449)

    import pennylane.estimator as qre
    
    data = [[0, 1, 0], [1, 1, 1], [1, 1, 0], [0, 0, 0], [0, 1, 0], [1, 1, 1], [1, 1, 0], [0, 0, 0]]
    bitstring_size = 3
    
    k = 2
    num_control_wires = 3
    num_work_wires = 1 + 1 + 3 * (1 << (num_control_wires - k) - 1)
    
    reg = qp.registers(
        {
            "control": num_control_wires,
            "target": bitstring_size,
            "work": num_work_wires
        }
    )
    
    dev = qp.device("null.qubit")
    @&#8203;qp.qnode(dev)
    def hybrid_qram():
        # prepare an address, e.g., |010> (index 2)
        qp.BasisEmbedding(2, wires=reg["control"])
    
        qp.HybridQRAM(
            data,
            control_wires=reg["control"],
            target_wires=reg["target"],
            work_wires=reg["work"],
            k=k
        )
        return qp.probs(wires=reg["target"])
    >>> qre.estimate(hybrid_qram)()
    --- Resources: ---
    Total wires: 12
      algorithmic wires: 11
      allocated wires: 1
        zero state: 1
        any state: 0
    Total gates : 2.797E+3
      'Toffoli': 142,
      'T': 2.112E+3,
      'CNOT': 262,
      'X': 65,
      'Hadamard': 216

Improvements 🛠

Decompositions 🍏

  • qp.transforms.decompose is now conveniently accessible from the top level as qp.decompose. (#​9011)

  • It is now possible to locally add decomposition rules to an operator via a qp.decomposition.local_decomps context manager. These rules will only be available within the context. (#​8955) (#​8998)

    @&#8203;qp.register_resources({qp.CNOT: 1, qp.H: 2})
    def custom_decomp(wires):
        qp.H(wires[1])
        qp.CNOT(wires)
        qp.H(wires[1])
    >>> with qp.decomposition.local_decomps():
    >>>    qp.add_decomps(qp.CZ, custom_decomp)
    >>>    print(qp.list_decomps(qp.CZ))
    Available Decomposition Rules:
    0: _cz_to_cps
    1: _cz_to_cnot
    2: _cz_to_ppr
    3: my_cz_rule
    >>> print(qp.list_decomps(qp.CZ))
    Available Decomposition Rules:
    0: _cz_to_cps
    1: _cz_to_cnot
    2: _cz_to_ppr
  • The following gate decompositions have been optimized for resource efficiency:

    • The QROM decomposition now has a more efficient allocation of work wires. (#​9131)

    • CSWAP is now decomposed more efficiently, using change_op_basis with two CNOT gates and a single Toffoli gate. (#​8887)

  • Several new decomposition rules have been added that can be accessed with enable_graph:

    • MultiControlledX has a new decomposition into a pair of TemporaryAND gates and one CNOT. It takes two control wires and at least one zeroed work wire that has been passed explicitly. (#​9291)

    • TemporaryAND can now be decomposed into the equivalent (although slightly more expensive) Toffoli gate. Note that this decomposition only is valid if TemporaryAND is used as intended - on zeroed input target qubits or zeroed output target qubits for Adjoint(TemporaryAND). (#​9303) (#​9424)

    • A new decomposition of Evolution into qp.PauliRot has been added which is compatible with the new graph-based decomposition system. Similarly, a decomposition of qp.RZ into qp.PhaseShift, including a global phase, has been added. (#​9001) (#​9049)

  • The graph-based decompositions system, enabled via enable_graph, now additionally supports the custom adjoint method of qutrit operators such as QutritUnitary, ControlledQutritUnitary, and TRZ. (#​9056)

  • The custom adjoint method of qutrit operators are implemented as decomposition rules compatible with the new graph-based decomposition system. (#​9056)

  • Now, when the new graph-based decomposition system is enabled, the decompose transform no longer tries to find a decomposition for an operator that meets a provided stopping_condition, even if it is not in the defined gate_set. (#​9036)

  • A strict keyword argument was added to the decompose transform that, when set to False, allows the decomposition graph to treat operators without a decomposition as part of the gate set. This prevents the decomposition graph from erroring out by keeping these operators in the circuit. (#​9025)

  • The inspectibility of general symbolic decomposition rules is improved. The string representation of a decomposition rule is by default its source code. Now for symbolic decomposition rules that wrap a base decomposition rule, the source code for the base decomposition rule is also displayed when printing this rule. (#​9305)

  • Allowed the passing of num_work_wires, alt_decomps and fixed_decomps to the device preprocessing function decompose, which are then passed through to the graph-based decomposition system. (#​9094)

  • The decomposition of BasisState is now compatible with qjit and jax.jit for static wires and/or states. Additionally, the parametric decomposition for traced states without qjit was updated to use powers of X rather than RX. (#​9069) (#​9124) (#​9339)

  • The decompositions of TemporaryAND, MultiRZ, and DiagonalQubitUnitary are now compatible with Catalyst. (#​9157)

  • The sk_decomposition now accepts "Adjoint(T)" and "Adjoint(S)" in the basis_set as a now-preferred alternative to the old "T*" and "S*" convention for gate adjoints. (#​9231)

  • Some decomposition rules for MultiControlledX (e.g., _mcx_two_borrowed_workers and _mcx_one_borrowed_worker) became identical for instances of this operator on less than 6 wires. To prevent this, stricter conditions have been applied on these decompositions. (#​9324)

  • Some wire reusage was removed in Select that was not consistent with the approach to work wires elsewhere in PennyLane, and that was not taken into account in the resource functions for the graph-based decomposition system (leading to decompositions not being resolved correctly). Also simplified the resource calculation of one decomposition of Select. (#​9222)

  • The decomposition of QSVT has been updated to be consistent with or without the graph-based decomposition system enabled. (#​8994)

  • When the new graph-based decomposition system is enabled, the decompose transform no longer raises duplicate warnings about operators that cannot be decomposed. (#​9025)

  • A new DecompositionWarning is now raised instead of a general UserWarning if the decomposition graph is unable to find a solution for an operator. (#​9001)

  • With the new graph-based decomposition system enabled, internal use of the decompose transform no longer raises warnings when the graph is unable to find a decomposition for an operator in the following scenarios: (#​9001)

    • circuit execution in the null.qubit device. - compilation with qp.compile. - gradient transforms within the expand_transform of hadamard_grad and param_shift.

    In these cases the operators will be treated as supported.

Disentangling Transforms 🧶

  • The disentangle_cnot and disentangle_swap are now callable from PennyLane, not just from Catalyst's passes module. These compilation passes simplify rendundant CNOT and SWAP gates. (#​9133)

    .. note::

    The disentangle_cnot and disentangle_swap are compilation passes that are only compatible with qjit workflows.

    Both compilation passes are designed to recognize patterns that include CNOT and SWAP gates that are redundant. In the case of disentangle_cnot, CNOT gates are replaced when the control wire is preceded by an X gate (and the control wire is guaranteed to be in the $\vert 1 \rangle$ state). This is illustrated in the example below, where no CNOT gates remain after the compilation pass is applied.

    import pennylane as qp
    
    dev = qp.device("lightning.qubit", wires=2)
    
    @&#8203;qp.qjit(capture=True)
    @&#8203;qp.transforms.disentangle_cnot
    @&#8203;qp.qnode(dev)
    def circuit():
        qp.X(0)
        qp.CNOT([0, 1])
        return qp.state()
    >>> print(qp.specs(circuit, level=1)().resources.gate_counts)
    {'PauliX': 2}

Drawing ✏️

  • The draw_graph function is now accessible from PennyLane, not just from Catalyst. This function allows for compact graphical inspection of qjit-compiled circuits, preserving structured control flow. (#​9020)

    .. note::

    The draw_graph function is only compatible with qjit workflows.

    Like with draw, draw_mpl, and specs, draw_graph can be given a level to inspect how compilation passes affect the circuit.

    import pennylane as qp
    
    @&#8203;qp.qjit(capture=True, autograph=True)
    @&#8203;qp.transforms.merge_rotations
    @&#8203;qp.transforms.cancel_inverses
    @&#8203;qp.qnode(qp.device("null.qubit", wires=3))
    def circuit(x):
        for i in range(10):
    
            w = i % 3
            qp.H(w)
            qp.H(w)
            qp.CNOT((i % 3, (i + 1) % 3))
    
            qp.RX(0.1, wires=w)
            qp.RX(0.2, wires=w)
    
        if x > 1.2:
            qp.Toffoli((0, 1, 2))
        else:
            qp.RZ(0.1, wires=2)
            qp.RZ(0.1, wires=2)
    
        return qp.expval(qp.X(0))

    After all optimizations are applied (level=2), we can still see the structure of the circuit.

    >>> fig, ax = qp.draw_graph(circuit, level=2)(0.1)
    >>> fig.savefig('path_to_file.png', dpi=300, bbox_inches="tight")

    Graphical representation of circuit

  • A new function called label has been added, which allows for attaching custom labels to operator instances for circuit drawing. (#​9078)

    @&#8203;qp.qnode(qp.device("default.qubit"))
    def circuit():
        qp.drawer.label(qp.H(0), "my-h")
        qp.CNOT([0, 1])
        return qp.probs()
    >>> print(qp.draw(circuit)())
    0: ──H("my-h")─╭●─┤  Probs
    1: ────────────╰X─┤  Probs

    In addition to this change, the mark was added to mark an operator as an input-encoding gate for circuit_spectrum, and qnode_spectrum.

Program Capture 📥

  • With program capture enabled and using for_loop and while_loop, constant closure variables with dynamic shapes can be used as such multiple times, no longer leading to leaked-tracer errors. (#​9275) (#​9335)

  • A more informative error is raised if something that is not a measurement process is returned from a QNode when program capture is turned on. (#​9072)

  • qp.cond converts non-boolean predicates to boolean immediately during capture time instead of from_plxpr doing this, allowing for easier maintenance and organization of from_plxpr. (#​9336)

  • qp.for_loop with negative step sizes is now handled immediately during capture time instead of handling this within from_plxpr, allowing for easier maintenance and organization of from_plxpr. (#​9299)

  • With program capture, dynamic-shaped arrays returned from qp.for_loop and qp.while_loop can now be combined with other dynamic-shaped arrays returned from qp.for_loop and qp.while_loop. (#​9245)

  • qp.vjp and qp.jvp are now compatible with program capture. (#​8736) (#​8788) (#​9019)

  • A qp.capture.subroutine has been added for jitting quantum subroutines with program capture. (#​8912)

  • qp.counts of mid-circuit measurement results is now compatible with program capture. (#​9022)

  • Program capture support for StatePrep and BasisState has been enhanced to accept state arguments of list or tuple types. (#​9338)

Catalyst Compatibility 🤝

  • BasisEmbedding is now captured as BasisState for compatibility with Catalyst and program capture. (#​9183)

  • The dynamic_one_shot and split_to_single_terms transforms are now compatible with qp.qjit. (#​9129)

  • BBQRAM, HybridQRAM, SelectOnlyQRAM and QROM now accept their classical data as a 2-dimensional array data type, which increases compatibility with Catalyst. (#​8791)

  • There is now one single source of truth for documentation of Catalyst passes while still maintaining accessibility from both PennyLane and Catalyst. This includes the following transforms: to_ppr, commute_ppr, merge_ppr_ppm, ppr_to_ppm, reduce_t_depth, decompose_arbitrary_ppr, ppm_compilation, and parity_synth. (#​9020) (#​9395) (#​9444)

    The source code for these passes in PennyLane has been removed as part of this change. However, all transforms listed above can still be accessed from the pennylane.transforms module as before (if Catalyst is installed: pip install pennylane-catalyst).

  • qp.math.givens_decomposition and qp.BasisRotation are now compatible with qjit when capture is disabled. (#​9155)

  • qp.value_and_grad is now available to simultaneously calculate the results and gradients in Catalyst. (#​8814)

  • Catalyst version information has been added to about. (#​9050)

Other improvements

  • Support has been added to assert_valid for decompositions that include mid-circuit measurements alongside better verification for the length of compared iterables. (#​9378)

  • A convenience function called ceil_log2 has been added, which computes the ceiling of the base-2 logarithm of its input and casts the result to an int. It is equivalent to int(np.ceil(np.log2(n))). (#​8972) (#​9069)

  • A new function called binary_decimals has been added to enable easy translation of rotation angles to the binary representation of their decimals. This is important for discretization steps, for example via phase gradient decompositions. (#​9117)

  • The binary_finite_reduced_row_echelon function was moved to a new file and now includes further linear algebraic functionalities over $\mathbb{Z}_2$. (#​8982)

    • binary_is_independent computes whether a vector is linearly independent of a basis of binary vectors over $\mathbb{Z}_2$. - binary_matrix_rank computes the rank over $\mathbb{Z}_2$ of a binary matrix. - binary_solve_linear_system solves a linear system of the form $A\cdot x=b$ with binary matrix $A$ and binary coefficient vector $b$ over $\mathbb{Z}_2$. - binary_select_basis selects linearly independent columns out of a collection of binary column vectors. The result forms a basis for the columnspace of the input. The columns that are not selected are returned as well.
  • A PauliSentence.prune and FermiSentence.prune method has been added, which removes terms with coefficients below a provided threshold. (#​9278)

  • Replaced the O(n²) incremental @= operator chaining in qp.pauli.string_to_pauli_word and qp.pauli.binary_to_pauli with a single qp.prod(*tuple_of_ops) call, collecting operators via generator expressions. These operators are now much faster for large Pauli strings. (#​9271)

  • Operations using PauliSentence are now much faster due to additional memorization in PauliWord.__hash__ (#​9261)

  • ZX-related transforms are now compatible with pyzx v0.10.0. (#​9179)

  • The to_zx transform is now compatible with the new graph-based decomposition system. (#​8994)

  • A new function called qp.decomposition.reconstruct has been added, which reconstructs the original operator instance from (*op.data, op.wires, **op.resource_params). This enables qjit-compatible symbolic decomposition rules that do not need to take an instance of the base operator as input. (#​9188)

  • The output of the qp.while_loop condition is now automatically converted to a bool. (#​9184)

  • A function for setting up transform inputs, including setting default values and basic validation, can now be provided to qp.transform via setup_inputs. (#​8732)

  • The unitary_to_rot transform now recursively decomposes QubitUnitary operations. This fixed a bug where two-qubit unitaries would decompose incorrectly to two single-qubit unitaries rather than their rotation decomposition. (#​9144)

  • Operations involving FermiWord objects are now significantly faster due to various performance enhancements made to the class. (#​9283)

  • MottonenStatePreparation now supports parameter broadcasting in its decomposition. (#​9148)

  • Circuits containing GlobalPhase are now trainable without removing the GlobalPhase. (#​8950)

  • Quantum functions defining quantum operations can now be passed to the compute_op, target_op and uncompute_op arguments of change_op_basis. (#​9163)

  • The qp.estimator.Resources class now has a better string representation in Jupyter Notebooks. (#​8880)

  • matrix can now also be applied to a sequence of operators. (#​8861)

  • A qp.workflow.get_compile_pipeline(qnode, level)(*args, **kwargs) function has been added to extract the CompilePipeline of a given QNode at a specific level. (#​8979) (#​9425)

  • No unnecessary classical registers will be created now when using qp.to_openqasm with measure_all=False. (#​9033)

  • Both "subroutines" and "custom_gates" are now always initialized in the QASM interpreter, resulting in more robust behaviour with PennyLane's QASM interpreter. (#​9201)

  • Applying qp.ctrl on Snapshot no longer produces a Controlled(Snapshot). Instead, it now returns the original Snapshot. (#​9001)

  • The default.qubit device now supports parameter-broadcasted GlobalPhase operations. (#​9148)

  • Global phases are now supported in from_qasm3 so that QASM including the gphase instruction can be interpreted. (#​9247)

Labs: a place for unified and rapid prototyping of research software 🧪

  • Added a new labs.estimator_beta for experimental development of resource estimation tools. Added various classes and functions to labs.estimator_beta to support advanced qubit management for resource estimation. Removed existing resource estimation functionality from the labs.resource_estimation module. (#​8996) (#​8868)

    • Allocate, allows users to allocate qubits in a resource decomposition. - Deallocate, allows users to deallocate qubits in a resource decomposition. - MarkClean, allows users to mark the state of qubits as the zero state in a circuit. - MarkQubits, allows users to mark the state of qubits in a circuit. - estimate_wires_from_circuit, estimates the number of additional qubits required from a circuit. - estimate_wires_from_resources, estimates the number of additional qubits required from a Resources object.
  • A new labs.estimator_beta.estimate() function has been created, which extends the functionality of qp.estimator.estimate() to utilize the advanced qubit management features for resource estimation. (#​9139)

  • Created a new LabsQROM resource operator in labs and added multiple alternate decompositions in labs for MultiControlledX that utilize the new qubit management features. (#​9258)

    • mcx_many_clean_aux_resource_decomp, uses multiple clean qubits to decompose. - mcx_one_clean_aux_resource_decomp, uses only one clean qubit to decompose. - mcx_one_dirty_aux_resource_decomp, uses only one dirty qubit to decompose.
  • Added alternate decompositions for CH and Hadamard operations in labs.estimator_beta to get optimal numbers. (#​9178)

  • Added comparator decompositions for RegisterEquality and OutOfPlaceIntegerComparator in labs.estimator_beta (#​9220)

  • Added alternate controlled decompositions for PauliRot and SelectPauliRot operations in labs.estimator_beta to get optimal numbers. (#​9186)

  • Added resource templates for state preparation operators, which include LabsMottonenStatePreparation, LabsCosineWindow, and LabsSumOfSlatersPrep. (#​9202)

  • Added various alternate resource decomposition functions for operators which make use of the phase gradient trick to accurately track auxiliary qubits using the new qubit management features. (#​9391)

  • Added custom phase gradient decomposition rules for RZ and SelectPauliRot.

    • make_rz_to_phase_gradient_decomp for RZ - make_selectpaulirot_to_phase_gradient_decomp for SelectPauliRot

    Their outputs can be passed as fixed_decomps in qp.decompose and are necessary for efficient discretization strategies in application algorithms. (#​9115)

  • The integration test for computing perturbation error of a compressed double-factorized (CDF) Hamiltonian in labs.trotter_error is upgraded to use a more realistic molecular geometry and a more reliable reference error. (#​8790)

Breaking changes 💔

  • The num_x_wires and num_work_wires arguments were added to the resource_keys and resource_params of SemiAdder. (#​9293)

    With this breaking change, please note the following:

    • Decomposition rules for SemiAdder now require those arguments. - When registering a resource function (qp.register_resources) to a decomposition rule of an operator that contains SemiAdder, the resource representation of SemiAdder must also receive these new arguments. - These changes are relevant only with enable_graph.
  • All Operator classes are now queued by default, unless they implement a custom queue method that changes this behaviour. (#​8131) (#​9029) (#​9423)

    This change also affects operators commonly used for operator math:

    • Hermitian - SProd - Sum - SparseHamiltonian - Projector - BasisStateProjector

    Note, however, that all Operator classes that are used to construct new operators are de-queued, so the following example does not illustrate the changed behaviour (i.e., creating B removes A from the queue in the example below):

    import pennylane as qp
    import numpy as np
    coeff = np.array([0.2, 0.1])
    
    @&#8203;qp.qnode(qp.device("lightning.qubit", wires=3))
    def expval(x: float):
        qp.RX(x, 1)
        A = qp.Hamiltonian(coeff, [qp.Y(1), qp.X(0)])
        B = A @&#8203; qp.Z(2)
        return qp.expval(B)
    >>> print(qp.draw(expval)(0.4))
    0: ───────────┤ ╭<𝓗(0.20,0.10)>
    1: ──RX(0.40)─┤ ├<𝓗(0.20,0.10)>
    2: ───────────┤ ╰<𝓗(0.20,0.10)>

    However, if we convert an operator A to numerical data, from which a new operator B is constructed, the chain of operator dependencies is broken and de-queuing will not work as previously expected:

    coeff = np.array([0.2, 0.1])
    
    @&#8203;qp.qnode(qp.device("lightning.qubit", wires=3))
    def expval(x: float):
        qp.RX(x, 1)
        A = qp.Hamiltonian(coeff, [qp.Y(1), qp.X(0)])
        numerical_data = A.matrix()
        B = qp.Hermitian(numerical_data, wires=[2, 0])
        return qp.expval(B)
    >>> print(qp.draw(expval, show_matrices=False)(0.4))
    0: ───────────╭𝓗(0.20,0.10)─┤ ╭<𝓗(M0)>
    1: ──RX(0.40)─╰𝓗(0.20,0.10)─┤ │
    2: ─────────────────────────┤ ╰<𝓗(M0)>

    As we can see, the Hamiltonian instance assigned to A remained in the queue. In cases where such a conversion to numerical data is unavoidable, perform the conversion outside of the quantum circuit.

  • Support for NumPy 1.x has been dropped following its end-of-life. NumPy 2.0 or higher is now required. (#​8914) (#​8954) (#​9017) (#​9167)

  • compute_qfunc_decomposition and has_qfunc_decomposition have been removed from Operator and all subclasses that implemented them. The graph decomposition system (enable_graph) should be used when capture is enabled. (#​8922)

  • The pennylane.devices.preprocess.mid_circuit_measurements transform has been removed. Instead, the device should determine which MCM method to use, and explicitly include dynamic_one_shot or defer_measurements in its preprocess transforms if necessary. See DefaultQubit.setup_execution_config and DefaultQubit.preprocess_transforms for an example. (#​8926)

  • The custom_decomps keyword argument to qp.device has been removed. Instead, with the graph decomposition system (enable_graph), new decomposition rules can be defined as quantum functions with registered resources. See pennylane.decomposition for more details. (#​8928)

    As an example, consider the case of running the following circuit, where we wish to convert CNOT gates into Hadamard and CZ gates.

    def circuit():
        qp.CNOT(wires=[0, 1])
        return qp.expval(qp.X(1))

    Instead of defining the CNOT decomposition as follows with custom_decomps,

    def custom_cnot(wires):
        return [
            qp.Hadamard(wires=wires[1]),
            qp.CZ(wires=[wires[0], wires[1]]),
            qp.Hadamard(wires=wires[1])
        ]
    
    dev = qp.device('default.qubit', wires=2, custom_decomps={"CNOT" : custom_cnot})
    qnode = qp.QNode(circuit, dev)

    the same result would now be obtained using:

    @&#8203;qp.decomposition.register_resources({qp.H: 2, qp.CZ: 1})
    def _custom_cnot_decomposition(wires, **_):
        qp.Hadamard(wires=wires[1])
        qp.CZ(wires=[wires[0], wires[1]])
        qp.Hadamard(wires=wires[1])
    
    qp.decomposition.add_decomps(qp.CNOT, _custom_cnot_decomposition)
    
    qp.decomposition.enable_graph()
    
    @&#8203;qp.transforms.decompose(gate_set={qp.CZ, qp.H})
    def circuit():
        qp.CNOT(wires=[0, 1])
        return qp.expval(qp.X(1))
    
    dev = qp.device('default.qubit', wires=2)
    qnode = qp.QNode(circuit, dev)
    >>> print(qp.draw(qnode, level="device")())
    0: ────╭●────┤
    1: ──H─╰Z──H─┤  <X>
  • The pennylane.operation.Operator.is_hermitian property has been removed and replaced with pennylane.operation.Operator.is_verified_hermitian, as it better reflects the functionality of this property. Alternatively, consider using the pennylane.is_hermitian function instead as it provides a more reliable check for hermiticity. Please be aware that it comes with a higher computational cost. (#​8919)

  • Passing a function to the gate_set argument in pennylane.decompose has been removed. The gate_set argument expects a static iterable of operator type and/or operator names, and the function should be passed to the stopping_condition argument instead. (#​8919)

  • argnum has been renamed argnums in qp.grad, qp.jacobian, qp.jvp, and qp.vjp to better match Catalyst and JAX. (#​8919)

  • Access to the following functions and classes from the pennylane.resources module has been removed. Instead, these functions must be imported from the pennylane.estimator module. (#​8919)

    • qp.estimator.estimate_shots in favor of qp.resources.estimate_shots - qp.estimator.estimate_error in favor of qp.resources.estimate_error - qp.estimator.FirstQuantization in favor of qp.resources.FirstQuantization - qp.estimator.DoubleFactorization in favor of qp.resources.DoubleFactorization

Deprecations 👋

  • The pennylane.workflow.get_transform_program function has been deprecated and will be removed in v0.46. Instead, please use the improved pennylane.workflow.get_compile_pipeline to retrieve the compilation pipeline of a QNode. (#​9077)

  • The id keyword argument to several classes has been renamed or removed entirely, and those changes will be official in v0.46. (#​8951) (#​9051)

    • The id keyword argument to MeasureNode and PrepareNode has been renamed to node_uid. - The id keyword argument to MidMeasure has been renamed to meas_uid. - The id keyword argument to MeasurementProcess will be removed. - The id keyword argument to Operator has been deprecated and will be removed.

    The id argument previously served two purposes: (1) adding custom labels to operator instances which were rendered in circuit drawings and (2) tagging encoding gates for Fourier spectrum analysis.

    These are now handled by dedicated functions:

    .. warning::

    Neither of these functions are supported in a qjit-compiled circuit, as the original behaviour was never supported.

    • Use label to attach a custom label to an operator instance for circuit drawing:

      # Legacy method (deprecated):
      qp.RX(0.5, wires=0, id="my-rx")
      
      # New method:
      qp.drawer.label(qp.RX(0.5, wires=0), "my-rx")
    • Use mark to mark an operator as an input-encoding gate for circuit_spectrum, and qnode_spectrum:

      # Legacy method (deprecated):
      qp.RX(0.5, wires=0, id="x0")
      
      # New method:
      qp.fourier.mark(qp.RX(0.5, wires=0), "x0")
  • Setting _queue_category=None in an operator class in order to deactivate its instances being queued has been deprecated. Implement a custom queue method for the respective class instead. Operator classes that used to have _queue_category=None have been updated to _queue_category="_ops", so that they are queued now. (#​8131)

  • The BoundTransform.transform property has been deprecated. Use BoundTransform.tape_transform instead. (#​8985)

  • expand and related functions (expand_tape, expand_tape_state_prep, and create_expand_trainable_multipar) have been deprecated and will be removed in v0.46. Instead, please use the qp.transforms.decompose transform for decomposing circuits. (#​8943) (#​9438)

  • Providing a value of None to aux_wire of qp.gradients.hadamard_grad with mode="reversed" or mode="standard" has been deprecated and will no longer be supported in v0.46. An aux_wire will no longer be automatically assigned. (#​8905)

  • The qp.transforms.create_expand_fn function has been deprecated and will be removed in v0.46. Instead, please use the qp.transforms.decompose function for decomposing circuits. (#​8941) (#​8977) [(

Note

PR body was truncated to here.


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • "every weekend"
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about these updates again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate Bot added dependencies Updates to project dependencies python Python related changes labels Jun 6, 2026
@renovate renovate Bot force-pushed the renovate/pennylane-0.x branch from 15df62f to d4eec96 Compare June 11, 2026 16:33
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
@renovate renovate Bot force-pushed the renovate/pennylane-0.x branch from d4eec96 to 6e56b8a Compare June 15, 2026 02:51
@renovate renovate Bot changed the title ⬆️🐍 Update dependency pennylane to >=0.45,<0.46 ⬆️🐍 Update dependency pennylane to ~=0.45.0 Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Updates to project dependencies python Python related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants