Skip to content

Commit

Permalink
Add a combine_global_phases transform (#6686)
Browse files Browse the repository at this point in the history
**Description of the Change:**
I implemented a `qml.transform.combine_global_phases` transform to
combine all `qml.GlobalPhase` gates in a circuit into a single
`qml.GlobalPhase` operation (applied at the end of the new circuit
without specifying any wire) with the phase equal to the total algebraic
sum of each original phase.


**Benefits:**
This transform can be useful for circuits that include a lot of
`qml.GlobalPhase` gates, which can be introduced directly during circuit
creation, decompositions that include `qml.GlobalPhase` gates, etc.


**Related GitHub Issues:** #6644

---------

Co-authored-by: Astral Cai <[email protected]>
Co-authored-by: Christina Lee <[email protected]>
Co-authored-by: Mudit Pandey <[email protected]>
  • Loading branch information
4 people authored Dec 16, 2024
1 parent a703355 commit b4f2c20
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 0 deletions.
6 changes: 6 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@
The functionality `qml.poly_to_angles` has been also extended to support GQSP.
[(#6565)](https://github.com/PennyLaneAI/pennylane/pull/6565)

* Added a new `qml.transforms.combine_global_phases` transform to combine all `qml.GlobalPhase` gates in a circuit into a single one applied at the end.
This can be useful for circuits that include a lot of `qml.GlobalPhase` gates, which can be introduced directly during circuit creation,
decompositions that include `qml.GlobalPhase` gates, etc.
[(#6686)](https://github.com/PennyLaneAI/pennylane/pull/6686)

* Added support to build a vibrational Hamiltonian in Taylor form.
[(#6523)](https://github.com/PennyLaneAI/pennylane/pull/6523)

Expand Down Expand Up @@ -522,6 +527,7 @@ Diksha Dhawan,
Lasse Dierich,
Lillian Frederiksen,
Pietropaolo Frisoni,
Simone Gasperini,
Austin Huang,
Korbinian Kottmann,
Christina Lee,
Expand Down
2 changes: 2 additions & 0 deletions pennylane/transforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
~transforms.pattern_matching_optimization
~transforms.transpile
~transforms.decompose
~transforms.combine_global_phases
There are also utility functions and decompositions available that assist with
both transforms, and decompositions within the larger PennyLane codebase.
Expand Down Expand Up @@ -305,6 +306,7 @@ def circuit(params):
from .split_non_commuting import split_non_commuting
from .split_to_single_terms import split_to_single_terms
from .insert_ops import insert
from .combine_global_phases import combine_global_phases

from .mitigate import (
mitigate_with_zne,
Expand Down
89 changes: 89 additions & 0 deletions pennylane/transforms/combine_global_phases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright 2018-2024 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.

"""
Provides a transform to combine all ``qml.GlobalPhase`` gates in a circuit into a single one applied at the end.
"""

import pennylane as qml
from pennylane.tape import QuantumScript, QuantumScriptBatch
from pennylane.transforms import transform
from pennylane.typing import PostprocessingFn


@transform
def combine_global_phases(tape: QuantumScript) -> tuple[QuantumScriptBatch, PostprocessingFn]:
"""Combine all ``qml.GlobalPhase`` gates into a single ``qml.GlobalPhase`` operation.
This transform returns a new circuit where all ``qml.GlobalPhase`` gates in the original circuit (if exists)
are removed, and a new ``qml.GlobalPhase`` is added at the end of the list of operations with its phase
being a total global phase computed as the algebraic sum of all global phases in the original circuit.
Args:
tape (QNode or QuantumScript or Callable): the input circuit to be transformed.
Returns:
qnode (QNode) or quantum function (Callable) or tuple[List[QuantumScript], function]:
the transformed circuit as described in :func:`qml.transform <pennylane.transform>`.
**Example**
Suppose we want to combine all the global phase gates in a given quantum circuit.
The ``combine_global_phases`` transform can be used to do this as follows:
.. code-block:: python3
dev = qml.device("default.qubit", wires=3)
@qml.transforms.combine_global_phases
@qml.qnode(dev)
def circuit():
qml.GlobalPhase(0.3, wires=0)
qml.PauliY(wires=0)
qml.Hadamard(wires=1)
qml.CNOT(wires=(1,2))
qml.GlobalPhase(0.46, wires=2)
return qml.expval(qml.X(0) @ qml.Z(1))
To check the result, let's print out the circuit:
>>> print(qml.draw(circuit)())
0: ──Y─────GlobalPhase(0.76)─┤ ╭<X@Z>
1: ──H─╭●──GlobalPhase(0.76)─┤ ╰<X@Z>
2: ────╰X──GlobalPhase(0.76)─┤
"""

has_global_phase = False
phi = 0
operations = []
for op in tape.operations:
if isinstance(op, qml.GlobalPhase):
has_global_phase = True
phi += op.parameters[0]
else:
operations.append(op)

if has_global_phase:
with qml.QueuingManager.stop_recording():
operations.append(qml.GlobalPhase(phi=phi))

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 ``QuantumScript``.
"""
return results[0]

return (new_tape,), null_postprocessing
174 changes: 174 additions & 0 deletions tests/transforms/test_combine_global_phases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"""
Tests for the combine_global_phases transform.
"""

import numpy as np
import pytest

import pennylane as qml
from pennylane.transforms import combine_global_phases


def original_qfunc(phi1, phi2, return_state=False):
qml.Hadamard(wires=1)
qml.GlobalPhase(phi1, wires=[0, 1])
qml.PauliY(wires=0)
qml.PauliX(wires=2)
qml.CNOT(wires=[1, 2])
qml.GlobalPhase(phi2, wires=1)
qml.CNOT(wires=[2, 0])
if return_state:
return qml.state()
return qml.expval(qml.Z(0) @ qml.X(1))


def expected_qfunc(phi1, phi2, return_state=False):
qml.Hadamard(wires=1)
qml.PauliY(wires=0)
qml.PauliX(wires=2)
qml.CNOT(wires=[1, 2])
qml.CNOT(wires=[2, 0])
qml.GlobalPhase(phi1 + phi2)
if return_state:
return qml.state()
return qml.expval(qml.Z(0) @ qml.X(1))


def test_no_global_phase_gate():
"""Test that when the input ``QuantumScript`` has no ``qml.GlobalPhase`` gate, the returned output is exactly the same"""
qscript = qml.tape.QuantumScript([qml.Hadamard(0), qml.RX(0, 0)])

expected_qscript = qml.tape.QuantumScript([qml.Hadamard(0), qml.RX(0, 0)])
(transformed_qscript,), _ = combine_global_phases(qscript)

qml.assert_equal(expected_qscript, transformed_qscript)


def test_single_global_phase_gate():
"""Test that when the input ``QuantumScript`` has a single ``qml.GlobalPhase`` gate, the returned output has an equivalent
``qml.GlobalPhase`` operation appended at the end"""
phi = 1.23
qscript = qml.tape.QuantumScript([qml.Hadamard(0), qml.GlobalPhase(phi, 0), qml.RX(0, 0)])

expected_qscript = qml.tape.QuantumScript([qml.Hadamard(0), qml.RX(0, 0), qml.GlobalPhase(phi)])
(transformed_qscript,), _ = combine_global_phases(qscript)

qml.assert_equal(expected_qscript, transformed_qscript)


def test_multiple_global_phase_gates():
"""Test that when the input ``QuantumScript`` has multiple ``qml.GlobalPhase`` gates, the returned output has an equivalent
single ``qml.GlobalPhase`` operation appended at the end with a total phase being equal to the sum of each original global phase
"""
phi1 = 1.23
phi2 = 4.56
qscript = qml.tape.QuantumScript(
[qml.GlobalPhase(phi1, 0), qml.Hadamard(0), qml.GlobalPhase(phi2, 0), qml.RX(0, 0)]
)

expected_qscript = qml.tape.QuantumScript(
[qml.Hadamard(0), qml.RX(0, 0), qml.GlobalPhase(phi1 + phi2)]
)
(transformed_qscript,), _ = combine_global_phases(qscript)

qml.assert_equal(expected_qscript, transformed_qscript)


def test_combine_global_phases():
"""Test that the ``combine_global_phases`` function implements the expected transform on a
QuantumScript and check the equivalence between statevectors before and after the transform."""
transformed_qfunc = combine_global_phases(original_qfunc)

dev = qml.device("default.qubit", wires=3)
original_qnode = qml.QNode(original_qfunc, device=dev)
transformed_qnode = qml.QNode(transformed_qfunc, device=dev)

phi1 = 1.23
phi2 = 4.56
expected_qscript = qml.tape.make_qscript(expected_qfunc)(phi1, phi2)
transformed_qscript = qml.tape.make_qscript(transformed_qfunc)(phi1, phi2)

original_state = original_qnode(phi1, phi2, return_state=True)
transformed_state = transformed_qnode(phi1, phi2, return_state=True)

# check the equivalence between expected and transformed quantum scripts
qml.assert_equal(expected_qscript, transformed_qscript)

# check the equivalence between statevectors before and after the transform
assert np.allclose(original_state, transformed_state)


@pytest.mark.autograd
def test_differentiability_autograd():
"""Test that the output of the ``combine_global_phases`` transform is differentiable with autograd"""
import pennylane.numpy as pnp

dev = qml.device("default.qubit", wires=3)
original_qnode = qml.QNode(original_qfunc, device=dev)
transformed_qnode = combine_global_phases(original_qnode)

phi1 = pnp.array(0.25)
phi2 = pnp.array(-0.6)
grad1, grad2 = qml.jacobian(transformed_qnode)(phi1, phi2)

assert qml.math.isclose(grad1, 0.0)
assert qml.math.isclose(grad2, 0.0)


@pytest.mark.jax
@pytest.mark.parametrize("use_jit", [False, True])
def test_differentiability_jax(use_jit):
"""Test that the output of the ``combine_global_phases`` transform is differentiable with JAX"""
import jax
import jax.numpy as jnp

dev = qml.device("default.qubit", wires=3)
original_qnode = qml.QNode(original_qfunc, device=dev)
transformed_qnode = combine_global_phases(original_qnode)

if use_jit:
transformed_qnode = jax.jit(transformed_qnode)

phi1 = jnp.array(0.25)
phi2 = jnp.array(-0.6)
grad1, grad2 = jax.jacobian(transformed_qnode, argnums=[0, 1])(phi1, phi2)

assert qml.math.isclose(grad1, 0.0)
assert qml.math.isclose(grad2, 0.0)


@pytest.mark.torch
def test_differentiability_torch():
"""Test that the output of the ``combine_global_phases`` transform is differentiable with Torch"""
import torch
from torch.autograd.functional import jacobian

dev = qml.device("default.qubit", wires=3)
original_qnode = qml.QNode(original_qfunc, device=dev)
transformed_qnode = combine_global_phases(original_qnode)

phi1 = torch.tensor(0.25)
phi2 = torch.tensor(-0.6)
grad1, grad2 = jacobian(transformed_qnode, (phi1, phi2))

zero = torch.tensor(0.0)
assert qml.math.isclose(grad1, zero)
assert qml.math.isclose(grad2, zero)


@pytest.mark.tf
def test_differentiability_tensorflow():
"""Test that the output of the ``combine_global_phases`` transform is differentiable with TensorFlow"""
import tensorflow as tf

dev = qml.device("default.qubit", wires=3)
original_qnode = qml.QNode(original_qfunc, device=dev)

phi1 = tf.Variable(0.25)
phi2 = tf.Variable(-0.6)
with tf.GradientTape() as tape:
transformed_qnode = combine_global_phases(original_qnode)(phi1, phi2)
grad1, grad2 = tape.jacobian(transformed_qnode, (phi1, phi2))

assert qml.math.isclose(grad1, 0.0)
assert qml.math.isclose(grad2, 0.0)

0 comments on commit b4f2c20

Please sign in to comment.