Skip to content

Commit

Permalink
commuting_evolution+tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dwierichs committed May 13, 2024
1 parent 3b6e170 commit 593aa8a
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 94 deletions.
16 changes: 3 additions & 13 deletions pennylane/ops/functions/equal.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,11 +324,9 @@ def _equal_operators(
atol=1e-9,
):
"""Default function to determine whether two Operator objects are equal."""
print("here")
if not isinstance(
op2, type(op1)
): # clarifies cases involving PauliX/Y/Z (Observable/Operation)
print("oh no")
return False

if isinstance(op1, qml.Identity):
Expand All @@ -338,26 +336,21 @@ def _equal_operators(
return True

if op1.arithmetic_depth != op2.arithmetic_depth:
print("oh no2")
return False

if op1.arithmetic_depth > 0:
# Other dispatches cover cases of operations with arithmetic depth > 0.
# If any new operations are added with arithmetic depth > 0, a new dispatch
# should be created for them.
print("oh no3")
return False
if not all(
qml.math.allclose(d1, d2, rtol=rtol, atol=atol) for d1, d2 in zip(op1.data, op2.data)
):
print("oh no4")
return False
if op1.wires != op2.wires:
print("oh no5")
return False

if op1.hyperparameters != op2.hyperparameters:
print("oh no6")
return False

if check_trainability:
Expand Down Expand Up @@ -442,7 +435,7 @@ def _equal_adjoint(op1: Adjoint, op2: Adjoint, **kwargs):
# pylint: disable=unused-argument
def _equal_scalar_symbolic_op(op1: ScalarSymbolicOp, op2: ScalarSymbolicOp, **kwargs):
"""Determine whether two Exp objects are equal"""

if not qml.math.allclose(op1.scalar, op2.scalar, rtol=kwargs["rtol"], atol=kwargs["atol"]):
return False
if kwargs["check_trainability"]:
Expand All @@ -453,18 +446,15 @@ def _equal_scalar_symbolic_op(op1: ScalarSymbolicOp, op2: ScalarSymbolicOp, **kw
return False
return qml.equal(op1.base, op2.base, **kwargs)


@_equal.register
# pylint: disable=unused-argument
def _equal_sprod(op1: SProd, op2: SProd, **kwargs):
"""Determine whether two SProd objects are equal"""
rtol, atol = (kwargs["rtol"], kwargs["atol"])

if op1.pauli_rep is not None and (op1.pauli_rep == op2.pauli_rep): # shortcut check
return True

if not qml.math.allclose(op1.scalar, op2.scalar, rtol=rtol, atol=atol):
return False
return qml.equal(op1.base, op2.base)
return _equal_scalar_symbolic_op(op1, op2, **kwargs)


@_equal.register
Expand Down
19 changes: 15 additions & 4 deletions pennylane/templates/subroutines/commuting_evolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
"""
# pylint: disable-msg=too-many-arguments,import-outside-toplevel
import pennylane as qml
from pennylane.operation import AnyWires, ParameterFrequenciesUndefinedError
from pennylane.operation import AnyWires, ParameterFrequenciesUndefinedError, MatrixUndefinedError
from pennylane.ops.op_math import ScalarSymbolicOp
from pennylane.typing import TensorLike


class CommutingEvolution(ScalarSymbolicOp):
Expand Down Expand Up @@ -131,6 +132,8 @@ def __init__(self, hamiltonian, time, frequencies=None, shifts=None, id=None):
recipe = qml.math.stack([c, qml.math.ones_like(c), s]).T
self.grad_recipe = (recipe,) + (None,) * len(hamiltonian.data)
self.grad_method = "A"
else:
self.grad_recipe = (None,) * (1 + len(hamiltonian.data))

super().__init__(hamiltonian, scalar=time, id=id)
self.hyperparameters["frequencies"] = frequencies
Expand All @@ -142,17 +145,26 @@ def parameter_frequencies(self):
# Note that because of the coefficients of the Hamiltonian, we do not have
# parameter_frequencies even if "frequencies" are provided at initialization!
raise ParameterFrequenciesUndefinedError(
"CommutingEvolution only has no parameter frequencies defined."
"CommutingEvolution has no parameter frequencies defined."
)

@property
def _queue_category(self):
return "_ops"

# pylint: disable=arguments-renamed, invalid-overridden-method
@property
def has_matrix(self):
return False

@staticmethod
def _matrix(scalar, mat):
return qml.math.expm(-1j * scalar * mat)

def matrix(self, wire_order=None) -> TensorLike:
"""Raise a MatrixUndefinedError for now to force decomposition on DefaultQubit."""
raise MatrixUndefinedError("CommutingEvolution does not define a matrix itself.")

# pylint: disable=invalid-overridden-method, arguments-renamed
@property
def has_decomposition(self):
Expand All @@ -170,7 +182,6 @@ def decomposition(self):
def adjoint(self):
frequencies = self.hyperparameters["frequencies"]
shifts = self.hyperparameters["shifts"]

return CommutingEvolution(self.base, -self.scalar, frequencies, shifts)

# pylint: disable=arguments-renamed,invalid-overridden-method
Expand All @@ -184,7 +195,7 @@ def diagonalizing_gates(self):
# pylint: disable=arguments-renamed, invalid-overridden-method
@property
def has_generator(self):
return self.base.is_hermitian and not np.real(self.coeff)
return True

def generator(self):
r"""Generator of an operator that is in single-parameter-form.
Expand Down
203 changes: 126 additions & 77 deletions tests/templates/test_subroutines/test_commuting_evolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,113 +22,163 @@
from pennylane import numpy as np
import copy

from pennylane.operation import ParameterFrequenciesUndefinedError

def test_standard_validity():
"""Run standard tests of operation validity."""
H = 2.0 * qml.PauliX(0) @ qml.PauliY(1) + 3.0 * qml.PauliY(0) @ qml.PauliZ(1)
time = 0.5
frequencies = (2, 4)
shifts = (1, 0.5)
op = qml.CommutingEvolution(H, time, frequencies=frequencies, shifts=shifts)
qml.ops.functions.assert_valid(op)

class TestBasicProperties:
"""Test basic methods/attributes/properties of CommutingEvolution."""

# pylint: disable=protected-access
def test_flatten_unflatten():
"""Unit tests for the flatten and unflatten methods."""
H = 2.0 * qml.PauliX(0) @ qml.PauliY(1) + 3.0 * qml.PauliY(0) @ qml.PauliZ(1)
time = 0.5
frequencies = (2, 4)
shifts = (1, 0.5)
op = qml.CommutingEvolution(H, time, frequencies=frequencies, shifts=shifts)
data, metadata = op._flatten()
def test_standard_validity(self):
"""Run standard tests of operation validity."""
H = 2.0 * qml.PauliX(0) @ qml.PauliY(1) + 3.0 * qml.PauliY(0) @ qml.PauliZ(1)
time = 0.5
frequencies = (2, 4)
shifts = (1, 0.5)
op = qml.CommutingEvolution(H, time, frequencies=frequencies, shifts=shifts)
qml.ops.functions.assert_valid(op)

# pylint: disable=protected-access
def test_flatten_unflatten(self):
"""Unit tests for the flatten and unflatten methods."""
H = 2.0 * qml.PauliX(0) @ qml.PauliY(1) + 3.0 * qml.PauliY(0) @ qml.PauliZ(1)
time = 0.5
frequencies = (2, 4)
shifts = (1, 0.5)
op = qml.CommutingEvolution(H, time, frequencies=frequencies, shifts=shifts)
data, metadata = op._flatten()

assert hash(metadata)

assert len(data) == 2
assert data[1] is H
assert data[0] == time
assert metadata == (frequencies, shifts)

assert hash(metadata)
new_op = type(op)._unflatten(*op._flatten())
assert qml.equal(op, new_op)
assert op is not new_op

assert len(data) == 2
assert data[1] is H
assert data[0] == time
assert metadata == (frequencies, shifts)
def test_adjoint(self):
"""Tests the CommutingEvolution.adjoint method provides the correct adjoint operation."""

n_wires = 2
dev = qml.device("default.qubit", wires=n_wires)

new_op = type(op)._unflatten(*op._flatten())
assert qml.equal(op, new_op)
assert op is not new_op
obs = [qml.PauliX(0) @ qml.PauliY(1), qml.PauliY(0) @ qml.PauliX(1)]
coeffs = [1, -1]
hamiltonian = qml.Hamiltonian(coeffs, obs)
frequencies = (2,)

@qml.qnode(dev)
def adjoint_evolution_circuit(time):
for i in range(n_wires):
qml.Hadamard(i)
qml.adjoint(qml.CommutingEvolution)(hamiltonian, time, frequencies)
return qml.expval(qml.PauliZ(1)), qml.state()

def test_adjoint():
"""Tests the CommutingEvolution.adjoint method provides the correct adjoint operation."""
@qml.qnode(dev)
def evolution_circuit(time):
for i in range(n_wires):
qml.Hadamard(i)
qml.CommutingEvolution(hamiltonian, time, frequencies)
return qml.expval(qml.PauliZ(1)), qml.state()

n_wires = 2
dev = qml.device("default.qubit", wires=n_wires)
res1, state1 = evolution_circuit(0.13)
res2, state2 = adjoint_evolution_circuit(-0.13)

obs = [qml.PauliX(0) @ qml.PauliY(1), qml.PauliY(0) @ qml.PauliX(1)]
coeffs = [1, -1]
hamiltonian = qml.Hamiltonian(coeffs, obs)
frequencies = (2,)
assert res1 == res2
assert all(np.isclose(state1, state2))

@qml.qnode(dev)
def adjoint_evolution_circuit(time):
for i in range(n_wires):
qml.Hadamard(i)
qml.adjoint(qml.CommutingEvolution)(hamiltonian, time, frequencies)
return qml.expval(qml.PauliZ(1)), qml.state()
def test_queuing(self):
"""Test that CommutingEvolution de-queues the input hamiltonian."""

@qml.qnode(dev)
def evolution_circuit(time):
for i in range(n_wires):
qml.Hadamard(i)
qml.CommutingEvolution(hamiltonian, time, frequencies)
return qml.expval(qml.PauliZ(1)), qml.state()
with qml.queuing.AnnotatedQueue() as q:
H = qml.X(0) + qml.Y(1)
op = qml.CommutingEvolution(H, 0.1, (2,))

res1, state1 = evolution_circuit(0.13)
res2, state2 = adjoint_evolution_circuit(-0.13)
assert len(q.queue) == 1
assert q.queue[0] is op
assert op._queue_category == "_ops"

assert res1 == res2
assert all(np.isclose(state1, state2))
def test_decomposition_expand(self):
"""Test that the decomposition of CommutingEvolution is an ApproxTimeEvolution with one step."""

hamiltonian = 0.5 * qml.PauliX(0) @ qml.PauliY(1)
time = 2.345

def test_queuing():
"""Test that CommutingEvolution de-queues the input hamiltonian."""
op = qml.CommutingEvolution(hamiltonian, time)

with qml.queuing.AnnotatedQueue() as q:
H = qml.X(0) + qml.Y(1)
op = qml.CommutingEvolution(H, 0.1, (2,))
assert op.has_decomposition is True

assert len(q.queue) == 1
assert q.queue[0] is op
decomp = op.decomposition()[0]

assert isinstance(decomp, qml.ApproxTimeEvolution)
assert qml.math.allclose(decomp.hyperparameters["hamiltonian"].data, hamiltonian.data)
assert decomp.hyperparameters["n"] == 1

def test_decomposition_expand():
"""Test that the decomposition of CommutingEvolution is an ApproxTimeEvolution with one step."""
tape = op.expand()
assert len(tape) == 1
assert isinstance(tape[0], qml.ApproxTimeEvolution)

hamiltonian = 0.5 * qml.PauliX(0) @ qml.PauliY(1)
time = 2.345
def test_matrix(self):
"""Test that the matrix of commuting evolution is the same as exponentiating -1j * t the hamiltonian."""

op = qml.CommutingEvolution(hamiltonian, time)
h = 2.34 * qml.PauliX(0)
time = 0.234
op = qml.CommutingEvolution(h, time)

decomp = op.decomposition()[0]
assert op.has_matrix is False

assert isinstance(decomp, qml.ApproxTimeEvolution)
assert qml.math.allclose(decomp.hyperparameters["hamiltonian"].data, hamiltonian.data)
assert decomp.hyperparameters["n"] == 1
mat = qml.matrix(op)
mat2 = op._matrix(time, qml.matrix(h))

tape = op.expand()
assert len(tape) == 1
assert isinstance(tape[0], qml.ApproxTimeEvolution)
expected = expm(-1j * time * qml.matrix(h))

assert qml.math.allclose(mat, expected)
assert qml.math.allclose(mat2, expected)

def test_matrix():
"""Test that the matrix of commuting evolution is the same as exponentiating -1j * t the hamiltonian."""
@pytest.mark.parametrize("frequencies", [None, (2, 4)])
def test_parameter_frequencies(self, frequencies):
"""Test that no parameter-frequencies are defined."""
h = 2.34 * qml.PauliX(0)
time = 0.234
op = qml.CommutingEvolution(h, time, frequencies=frequencies)
with pytest.raises(ParameterFrequenciesUndefinedError, match="CommutingEvolution has no"):
_ = op.parameter_frequencies

h = 2.34 * qml.PauliX(0)
time = 0.234
op = qml.CommutingEvolution(h, time)
@pytest.mark.parametrize("frequencies", [None, (1,)])
def test_grad_method_grad_recipe(self, frequencies):
"""Test that no parameter-frequencies are defined."""
h = 2.34 * qml.PauliX(0)
time = 0.234
op = qml.CommutingEvolution(h, time, frequencies=frequencies)

mat = qml.matrix(op)
if frequencies is None:
assert op.grad_method is None
assert op.grad_recipe == (None,) * 2
else:
assert op.grad_method == "A"
assert len(op.grad_recipe) == 2
assert qml.math.allclose(
op.grad_recipe[0], [[0.5, 1.0, np.pi / 2], [-0.5, 1.0, -np.pi / 2]]
)
assert op.grad_recipe[1] is None

expected = expm(-1j * time * qml.matrix(h))
def test_diagonalizing_gates(self):
"""Test the diagonalizing gates of CommutingEvolution."""
h = 2.34 * qml.PauliX(0)
time = 0.234
op = qml.CommutingEvolution(h, time)
assert op.has_diagonalizing_gates is True
assert op.diagonalizing_gates() == qml.PauliX(0).diagonalizing_gates()

assert qml.math.allclose(mat, expected)
def test_generator(self):
"""Test the generator of CommutingEvolution."""
h = 2.34 * qml.PauliX(0)
time = 0.234
op = qml.CommutingEvolution(h, time)
assert op.has_generator is True
assert op.generator() == -h


def test_forward_execution():
Expand Down Expand Up @@ -236,7 +286,6 @@ def circuit(time, coeffs):

x_vals = [np.array(x, requires_grad=True) for x in np.linspace(-np.pi, np.pi, num=10)]
circuit(x_vals[0], diff_coeffs)
print(circuit.tape[1].grad_recipe)

grads_finite_diff = [
np.hstack(qml.gradients.finite_diff(circuit)(x, diff_coeffs)) for x in x_vals
Expand Down

0 comments on commit 593aa8a

Please sign in to comment.