Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7fed855
move intermediate reps to mainline pennylane
Qottmann Sep 1, 2025
0a847a1
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane
Qottmann Sep 8, 2025
7c4179c
RZ Phrase Gradient transform
Qottmann Sep 8, 2025
028407b
RZ Phrase Gradient transform
Qottmann Sep 8, 2025
50d637f
tests
Qottmann Sep 8, 2025
c79180b
fix global phase
Qottmann Sep 8, 2025
fcf3284
be a bit more clever about global phases
Qottmann Sep 8, 2025
7c6c9d4
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
Qottmann Sep 8, 2025
193df79
remove unrelated things
Qottmann Sep 8, 2025
5ed02f6
docs
Qottmann Sep 8, 2025
790b611
global phase
Qottmann Sep 9, 2025
50dcd72
global phase
Qottmann Sep 9, 2025
de14a67
move queueing manager
Qottmann Sep 9, 2025
43339b5
Apply suggestions from code review
Qottmann Sep 9, 2025
556473e
update
Qottmann Sep 9, 2025
241494c
Merge branch 'phase-gradient' of https://github.com/PennyLaneAI/penny…
Qottmann Sep 9, 2025
319ca1a
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
Qottmann Sep 9, 2025
a64700c
run hooks
Qottmann Sep 9, 2025
377e480
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
Qottmann Sep 10, 2025
c626561
docs
Qottmann Sep 10, 2025
10f6c3d
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
Qottmann Sep 15, 2025
d6d6155
explain bits
Qottmann Sep 16, 2025
0631b8b
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
Qottmann Sep 16, 2025
2ddd4a0
docs
Qottmann Sep 16, 2025
f42074d
docs
Qottmann Sep 16, 2025
0585502
docs
Qottmann Sep 16, 2025
4f687c9
docs
Qottmann Sep 16, 2025
88a24ac
docs
Qottmann Sep 16, 2025
a67ad30
make it change-op-basis
Qottmann Sep 16, 2025
acbbb60
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
Qottmann Sep 17, 2025
adbffd3
angle_wires
Qottmann Sep 17, 2025
b606f9a
unify global phases
Qottmann Sep 17, 2025
2f8ffcf
validate wires
Qottmann Sep 17, 2025
6db4f1a
Apply suggestions from code review
Qottmann Sep 17, 2025
6d36354
Merge branch 'master' into phase-gradient
Qottmann Sep 18, 2025
e033a33
docs
Qottmann Sep 18, 2025
3206cf3
angilary lol
Qottmann Sep 18, 2025
a2c9a17
an is actually correct
Qottmann Sep 18, 2025
2e67e05
code review docs
Qottmann Sep 18, 2025
2117ada
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
Qottmann Sep 18, 2025
49ffe1a
changelog
Qottmann Sep 18, 2025
9a4bff6
trigger
Qottmann Sep 18, 2025
d401798
Merge branch 'master' of https://github.com/PennyLaneAI/pennylane int…
Qottmann Sep 19, 2025
8533e7e
Apply suggestions from code review
Qottmann Sep 19, 2025
2b9a9e0
code review
Qottmann Sep 19, 2025
b4f6bf0
Merge branch 'phase-gradient' of https://github.com/PennyLaneAI/penny…
Qottmann Sep 19, 2025
e1234f5
Merge branch 'master' into phase-gradient
Qottmann Sep 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,13 @@
* The :func:`~.transforms.decompose` transform is now able to decompose classically controlled operations.
[(#8145)](https://github.com/PennyLaneAI/pennylane/pull/8145)

* A new transform :func:`~.transforms.rz_phase_gradient` lets you realize arbitrary angle :class:`~.RZ` rotations
with a phase gradient resource state and semi-in-place addition (:class:`~.SemiAdder`). This can be a crucial
subroutine in FTQC when sufficient auxiliary wires are available, as it saves on T gates compared to other
discretization schemes.
[(#8213)](https://github.com/PennyLaneAI/pennylane/pull/8213)


<h3>Improvements 🛠</h3>

* :func:`pennylane.snapshots` can now be used with `mcm_method="one-shot"` and `mcm_method="tree-traversal"`.
Expand Down
2 changes: 2 additions & 0 deletions pennylane/transforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
~transforms.transpile
~transforms.undo_swaps
~transforms.unitary_to_rot
~transforms.rz_phase_gradient
~transforms.zx.optimize_t_count
~transforms.zx.push_hadamards
~transforms.zx.reduce_non_clifford
Expand Down Expand Up @@ -348,6 +349,7 @@ def circuit(params):
)
from .broadcast_expand import broadcast_expand
from .decompose import decompose
from .rz_phase_gradient import rz_phase_gradient


def __getattr__(name):
Expand Down
215 changes: 215 additions & 0 deletions pennylane/transforms/rz_phase_gradient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# Copyright 2025 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
A transform for decomposing RZ rotations using a phase gradient catalyst state.
"""
import numpy as np

import pennylane as qml
from pennylane.queuing import QueuingManager
from pennylane.tape import QuantumScript, QuantumScriptBatch
from pennylane.transforms import transform
from pennylane.typing import PostprocessingFn
from pennylane.wires import Wires


def _binary_repr_int(phi, precision):
# Reasoning for +1e-10 term:
# due to the division by pi, we obtain 14.999.. instead of 15 for, e.g., (1, 1, 1, 1) pi
# at the same time, we want to floor off any additional floats when converting to the desired precision,
# e.g. representing (1, 1, 1, 1) with only 3 digits we want to obtain (1, 1, 1)
# so overall we floor but make sure we add a little term to not accidentally write 14 when the result is 14.999..
return int(np.floor(2**precision * phi / (2 * np.pi) + 1e-10))


@QueuingManager.stop_recording()
def _rz_phase_gradient(
phi: float, wire: Wires, angle_wires: Wires, phase_grad_wires: Wires, work_wires: Wires
) -> tuple[QuantumScriptBatch, PostprocessingFn]:
"""Function that transforms the RZ gate to the phase gradient circuit
The precision is implicitly defined by the length of ``angle_wires``
Note that the global phases are collected and added as one big global phase in the main function
"""

precision = len(angle_wires)
# BasisEmbedding can handle integer inputs, no need to actually translate to binary
binary_int = _binary_repr_int(-phi, precision)

compute_op = qml.ctrl(qml.BasisEmbedding(features=binary_int, wires=angle_wires), control=wire)
target_op = qml.SemiAdder(angle_wires, phase_grad_wires, work_wires)

return qml.change_op_basis(compute_op, target_op, compute_op)


@transform
def rz_phase_gradient(
tape: QuantumScript, angle_wires: Wires, phase_grad_wires: Wires, work_wires: Wires
) -> tuple[QuantumScriptBatch, PostprocessingFn]:
r"""Quantum function transform to decompose all instances of :class:`~.RZ` gates into additions
using a phase gradient resource state.

For example, an :class:`~.RZ` gate with angle :math:`\phi = (0 \cdot 2^{-1} + 1 \cdot 2^{-2} + 0 \cdot 2^{-3}) 2\pi`
is translated into the following routine, where the angle is conditionally prepared on the ``angle_wires`` in binary
and added to a ``phase_grad_wires`` register semi-inplace via :class:`~.SemiAdder`.

.. code-block::

target: ─RZ(ϕ)─ = ────╭●──────────────╭●────exp(iϕ/2)─┤
ang_0: ────├|0⟩─╭SemiAdder─├|0⟩────────────┤
ang_1: ────├|1⟩─├SemiAdder─├|1⟩────────────┤
ang_2: ────╰|0⟩─├SemiAdder─╰|0⟩────────────┤
phg_0: ─────────├SemiAdder─────────────────┤
phg_1: ─────────├SemiAdder─────────────────┤
phg_2: ─────────╰SemiAdder─────────────────┤

For this routine to work, the provided ``phase_grad_wires`` need to hold a phase gradient
state :math:`|\nabla Z\rangle = \frac{1}{\sqrt{2^n}} \sum_{m=0}^{2^n-1} e^{2 \pi i \frac{m}{2^n}} |m\rangle`.
Because this state is not modified and can be re-used at a later stage, the transform does not prepare it but
rather assumes it has been prepared on those wires at an earlier stage.


Note that :class:`~.SemiAdder` requires additional ``work_wires`` (not shown in the diagram) for the semi-in-place addition
:math:`\text{SemiAdder}|x\rangle_\text{ang} |y\rangle_\text{phg} = |x\rangle_\text{ang} |x + y\rangle_\text{phg}`.

More details can be found on page 4 in `arXiv:1709.06648 <https://arxiv.org/abs/1709.06648>`__
and Figure 17a in `arXiv:2211.15465 <https://arxiv.org/abs/2211.15465>`__ (a generalization to
multiplexed :class:`~.RZ` rotations is provided in Figure 4 in
`arXiv:2409.07332 <https://arxiv.org/abs/2409.07332>`__).

Note that technically, this circuit realizes :class:`~.PhaseShift`, i.e. :math:`R_\phi(\phi) = R_Z(\phi) e^{i\phi/2}`.
The additional global phase is taken into account in the decomposition.

Args:
tape (QNode or QuantumTape or Callable): A quantum circuit containing :class:`~.RZ` gates.
angle_wires (Wires): The qubits that conditionally load the angle :math:`\phi` of
the :class:`~.RZ` gate in binary as a multiple of :math:`2\pi`.
The length of the ``angle_wires`` implicitly determines the precision
with which the angle is represented.
E.g., :math:`(2^{-1} + 2^{-2} + 2^{-3}) 2\pi` is exactly represented by three bits as ``111``.
phase_grad_wires (Wires): The catalyst qubits with a phase gradient state prepared on them.
Needs to be at least the length of ``angle_wires`` and will only
use the first ``len(angle_wires)``.
work_wires (Wires): Additional work wires to realize the :class:`~.SemiAdder` between the ``angle_wires`` and
``phase_grad_wires``. Needs to be at least ``b-1`` wires, where ``b=len(phase_grad_wires)`` is
the precision of the angle :math:`\phi`.

Returns:
qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform <pennylane.transform>`.

**Example**

.. code-block:: python

from functools import partial

import numpy as np

import pennylane as qml
from pennylane.transforms.rz_phase_gradient import rz_phase_gradient

precision = 3
phi = (1 / 2 + 1 / 4 + 1 / 8) * 2 * np.pi
wire = "targ"
angle_wires = [f"ang_{i}" for i in range(precision)]
phase_grad_wires = [f"phg_{i}" for i in range(precision)]
work_wires = [f"work_{i}" for i in range(precision - 1)]
wire_order = [wire] + angle_wires + phase_grad_wires + work_wires


def phase_gradient(wires):
# prepare phase gradient state
qml.X(wires[-1])
qml.QFT(wires)


@partial(
rz_phase_gradient,
angle_wires=angle_wires,
phase_grad_wires=phase_grad_wires,
work_wires=work_wires,
)
@qml.qnode(qml.device("default.qubit"))
def rz_circ(phi, wire):
phase_gradient(phase_grad_wires) # prepare phase gradient state

qml.Hadamard(wire) # transform rotation
qml.RZ(phi, wire)
qml.Hadamard(wire) # transform rotation

return qml.probs(wire)


In this example we perform the rotation of an angle of :math:`\phi = (0.111)_2 2\pi`. Because phase shifts
are trivial on computational basis states, we transform the :math:`R_Z` rotation to :math:`R_X = H R_Z H` via two
:class:`~.Hadamard` gates.

Note that for the transform to work, we need to also prepare a phase gradient state on the ``phase_grad_wires``.

Overall, the full circuit looks like the following:

>>> print(qml.draw(rz_circ, wire_order=wire_order)(phi, wire))
targ: ──H──────╭(|Ψ⟩)@SemiAdder@(|Ψ⟩)──H─╭GlobalPhase(2.75)─┤ Probs
ang_0: ─────────├(|Ψ⟩)@SemiAdder@(|Ψ⟩)────├GlobalPhase(2.75)─┤
ang_1: ─────────├(|Ψ⟩)@SemiAdder@(|Ψ⟩)────├GlobalPhase(2.75)─┤
ang_2: ─────────├(|Ψ⟩)@SemiAdder@(|Ψ⟩)────├GlobalPhase(2.75)─┤
phg_0: ────╭QFT─├(|Ψ⟩)@SemiAdder@(|Ψ⟩)────├GlobalPhase(2.75)─┤
phg_1: ────├QFT─├(|Ψ⟩)@SemiAdder@(|Ψ⟩)────├GlobalPhase(2.75)─┤
phg_2: ──X─╰QFT─├(|Ψ⟩)@SemiAdder@(|Ψ⟩)────├GlobalPhase(2.75)─┤
work_0: ─────────├(|Ψ⟩)@SemiAdder@(|Ψ⟩)────├GlobalPhase(2.75)─┤
work_1: ─────────╰(|Ψ⟩)@SemiAdder@(|Ψ⟩)────╰GlobalPhase(2.75)─┤

The additional work wires are required by the :class:`~.SemiAdder`.
Executing the circuit, we get the expected result:

>>> rz_circ(phi, wire)
array([0.85355339, 0.14644661])

"""

if len(phase_grad_wires) < len(angle_wires):
raise ValueError(
f"phase_grad_wires needs to be at least as large as angle_wires. Got {len(phase_grad_wires)} phase_grad_wires, which is fewer than the {len(angle_wires)} angle wires."
)

operations = []
global_phases = []
for op in tape.operations:
if isinstance(op, qml.RZ):
wire = op.wires
phi = op.parameters[0]
global_phases.append(phi / 2)

operations.append(
_rz_phase_gradient(
phi,
wire,
angle_wires=angle_wires,
phase_grad_wires=phase_grad_wires,
work_wires=work_wires,
)
)
else:
operations.append(op)

operations.append(qml.GlobalPhase(sum(global_phases)))

new_tape = tape.copy(operations=operations)

def null_postprocessing(results):
"""A postprocesing function returned by a transform that only converts the batch of results
into a result for a single ``QuantumTape``.
"""
return results[0]

return [new_tape], null_postprocessing
Loading