Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add qml.generator(op) function #2256

Merged
merged 60 commits into from
Mar 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
cb1fe52
Add operator transform functionality
josh146 Feb 28, 2022
931515f
Add matrix function:
josh146 Feb 28, 2022
17bf5ba
add tests
josh146 Feb 28, 2022
b5586b2
add tests
josh146 Feb 28, 2022
ef42ab2
add tests
josh146 Feb 28, 2022
2beaa40
add tests
josh146 Feb 28, 2022
1c49491
add tests
josh146 Feb 28, 2022
397c343
Merge branch 'op-transform' into matrix-function
josh146 Feb 28, 2022
3580cf9
add tests
josh146 Feb 28, 2022
e7c803c
add tests
josh146 Feb 28, 2022
955ca89
Merge branch 'master' into op-transform
josh146 Feb 28, 2022
0ae781c
more docs
josh146 Feb 28, 2022
94b2f5a
Merge branch 'op-transform' into matrix-function
josh146 Feb 28, 2022
da6278f
more
josh146 Feb 28, 2022
1018410
add tests
josh146 Feb 28, 2022
190a7d4
Apply suggestions from code review
josh146 Feb 28, 2022
6ce4f9b
Add qml.eigval() function
josh146 Feb 28, 2022
cf045e0
add test
josh146 Feb 28, 2022
80124c4
Merge branch 'matrix-function' of github.com:PennyLaneAI/pennylane in…
josh146 Feb 28, 2022
4ea7fb1
Merge branch 'matrix-function' into eigvals-function
josh146 Feb 28, 2022
a66e6e1
finish adding tests
josh146 Feb 28, 2022
f3d72c0
finish adding tests
josh146 Feb 28, 2022
874c3ca
Add qml.generator(op) function
josh146 Mar 1, 2022
502a105
fixes
josh146 Mar 1, 2022
8c2a303
more tests
josh146 Mar 1, 2022
5a88c52
more tests
josh146 Mar 1, 2022
b0161f9
fix
josh146 Mar 1, 2022
c19fffe
more tests
josh146 Mar 1, 2022
8d56cdf
Apply suggestions from code review
josh146 Mar 1, 2022
f85f106
Apply suggestions from code review
josh146 Mar 1, 2022
249ff25
Update tests/ops/functions/test_eigvals.py
josh146 Mar 1, 2022
780e943
Apply suggestions from code review
josh146 Mar 1, 2022
7b63422
update deprecation warning
josh146 Mar 1, 2022
6215b74
Update tests/ops/functions/test_eigvals.py
josh146 Mar 1, 2022
696504b
update deprecation warning
josh146 Mar 1, 2022
c6ca52f
Merge branch 'matrix-function' into eigvals-function
josh146 Mar 1, 2022
f3e219e
add another test
josh146 Mar 1, 2022
2f5762d
suggested changes
josh146 Mar 1, 2022
131e752
black
josh146 Mar 1, 2022
c7653ee
black
josh146 Mar 1, 2022
379aa56
Merge branch 'eigvals-function' into gen-fn
josh146 Mar 1, 2022
fbd3d94
Merge branch 'master' into matrix-function
josh146 Mar 2, 2022
1b1801a
Merge branch 'matrix-function' into eigvals-function
josh146 Mar 2, 2022
2282b76
Apply suggestions from code review
josh146 Mar 2, 2022
7109d2c
Merge branch 'eigvals-function' into gen-fn
josh146 Mar 2, 2022
e40a691
fix
josh146 Mar 2, 2022
3b79b7a
changelog
josh146 Mar 2, 2022
9688f82
more
josh146 Mar 2, 2022
3a54758
Merge branch 'master' into eigvals-function
josh146 Mar 2, 2022
0376469
Merge branch 'eigvals-function' into gen-fn
josh146 Mar 2, 2022
20c102d
bugfix
josh146 Mar 2, 2022
9749648
suggested changes
josh146 Mar 2, 2022
0e24463
change parameter-frequency logic
josh146 Mar 2, 2022
109a288
Apply suggestions from code review
josh146 Mar 2, 2022
a398f03
Merge branch 'eigvals-function' into gen-fn
josh146 Mar 2, 2022
5d7073e
Merge branch 'master' into eigvals-function
josh146 Mar 2, 2022
1a346b0
fix
josh146 Mar 2, 2022
ae410b7
Merge branch 'eigvals-function' into gen-fn
josh146 Mar 2, 2022
06a6e2e
Merge branch 'master' into gen-fn
josh146 Mar 2, 2022
dcf79f4
Merge branch 'master' into gen-fn
josh146 Mar 3, 2022
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
2 changes: 1 addition & 1 deletion doc/introduction/operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ and extracting information.
~pennylane.ctrl
~pennylane.matrix
~pennylane.eigvals

~pennylane.generator

All operator functions can be used on instantiated operators,

Expand Down
3 changes: 3 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
- `qml.eigvals()` for computing the eigenvalues of one or more operators.
[(#2248)](https://github.com/PennyLaneAI/pennylane/pull/2248)

- `qml.generator()` for computing the generator of a single-parameter unitary operation.
[(#2256)](https://github.com/PennyLaneAI/pennylane/pull/2256)

All operator transforms can be used on instantiated operators,

```pycon
Expand Down
5 changes: 2 additions & 3 deletions pennylane/fourier/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import numpy as np


from pennylane.utils import get_generator
import pennylane as qml


def format_nvec(nvec):
Expand Down Expand Up @@ -63,8 +63,7 @@ def get_spectrum(op, decimals):
Returns:
set[float]: non-negative frequencies contributed by this input-encoding gate
"""
matrix, coeff = get_generator(op, return_matrix=True)
matrix = coeff * matrix
matrix = qml.matrix(qml.generator(op, format="observable"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could have been done in #2182 already, but now that we're touching this code, we could simply use parameter_frequencies instead of this whole utility function? Might be a separate PR, though (maybe together with Rotosolve using parameter_frequencies, grouping it into an "application" PR).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dwierichs which would you prefer, doing it here, or doing it in a separate PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess a separate one that uses parameter_frequencies where applicable is more clean? :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep :)


# todo: use qml.math.linalg once it is tested properly
evals = np.linalg.eigvalsh(matrix)
Expand Down
41 changes: 16 additions & 25 deletions pennylane/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1291,23 +1291,15 @@ def parameter_frequencies(self):
if self.num_params == 1:
# if the operator has a single parameter, we can query the
# generator, and if defined, use its eigenvalues.
gen = self.generator()

try:
gen_eigvals = tuple(self.generator().eigvals())
return qml.gradients.eigvals_to_frequencies(gen_eigvals)

except (MatrixUndefinedError, EigvalsUndefinedError):

if isinstance(gen, qml.Hamiltonian):
mat = qml.utils.sparse_hamiltonian(gen).toarray()
eigvals = tuple(np.round(np.linalg.eigvalsh(mat), 8))
return qml.gradients.eigvals_to_frequencies(eigvals)
with warnings.catch_warnings():
warnings.filterwarnings(
action="ignore", message=r".+ eigenvalues will be computed numerically\."
)
eigvals = qml.eigvals(qml.generator(self, format="observable"))

if isinstance(gen, qml.SparseHamiltonian):
mat = gen.sparse_matrix().toarray()
eigvals = tuple(np.round(np.linalg.eigvalsh(mat), 8))
return qml.gradients.eigvals_to_frequencies(eigvals)
eigvals = tuple(np.round(eigvals, 8))
return qml.gradients.eigvals_to_frequencies(eigvals)
Comment on lines +1295 to +1302
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dwierichs I made the change here because I realized the old version was simply duplicating qml.eigvals(); this makes the method cleaner, and less likely to break in the future.

Copy link
Contributor

@dwierichs dwierichs Mar 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh great, this does look very nice :)


raise OperatorPropertyUndefined(
f"Operation {self.name} does not have parameter frequencies."
Expand Down Expand Up @@ -1398,14 +1390,13 @@ def __init__(self, *params, wires=None, do_queue=True, id=None):
super().__init__(*params, wires=wires, do_queue=do_queue, id=id)

# check the grad_recipe validity
if self.grad_method == "A":
if self.grad_recipe is None:
# default recipe for every parameter
self.grad_recipe = [None] * self.num_params
else:
assert (
len(self.grad_recipe) == self.num_params
), "Gradient recipe must have one entry for each parameter!"
if self.grad_recipe is None:
# default recipe for every parameter
self.grad_recipe = [None] * self.num_params
else:
assert (
len(self.grad_recipe) == self.num_params
), "Gradient recipe must have one entry for each parameter!"


class Channel(Operation, abc.ABC):
Expand Down Expand Up @@ -2394,8 +2385,8 @@ def operation_derivative(operation) -> np.ndarray:
ValueError: if the operation does not have a generator or is not composed of a single
trainable parameter
"""
generator, prefactor = qml.utils.get_generator(operation, return_matrix=True)
return 1j * prefactor * generator @ operation.get_matrix()
generator = qml.matrix(qml.generator(operation, format="observable"))
return 1j * generator @ operation.get_matrix()


@qml.BooleanFn
Expand Down
1 change: 1 addition & 0 deletions pennylane/ops/functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
This module contains functions that act on operators and tapes.
"""
from .eigvals import eigvals
from .generator import generator
from .matrix import matrix
180 changes: 180 additions & 0 deletions pennylane/ops/functions/generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Copyright 2018-2021 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.
"""
This module contains the qml.generator function.
"""
# pylint: disable=protected-access
import pennylane as qml


def _generator_observable(gen, op):
"""Return the generator as type :class:`~.Hermitian`,
:class:`~.SparseHamiltonian`, or :class:`~.Hamiltonian`,
as provided by the original gate.
"""
if isinstance(gen, (qml.Hermitian, qml.SparseHamiltonian)):
if not op.inverse:
return gen

param = gen.parameters[0]
wires = gen.wires

return gen.__class__(-param, wires=wires)

if op.inverse:
gen = -1.0 * gen

return gen


def _generator_hamiltonian(gen, op):
"""Return the generator as type :class:`~.Hamiltonian`."""
wires = op.wires

if isinstance(gen, qml.Hamiltonian):
H = gen

elif isinstance(gen, (qml.Hermitian, qml.SparseHamiltonian)):

if isinstance(gen, qml.Hermitian):
mat = gen.parameters[0]

elif isinstance(gen, qml.SparseHamiltonian):
mat = gen.parameters[0].toarray()

coeffs, obs = qml.utils.decompose_hamiltonian(mat, wire_order=wires, hide_identity=True)
H = qml.Hamiltonian(coeffs, obs)

elif isinstance(gen, qml.operation.Observable):
H = 1.0 * gen

if op.inverse:
H = -1.0 * H

return H


def _generator_prefactor(gen, op):
r"""Return the generator as ```(obs, prefactor)`` representing
:math:`G=p \hat{O}`, where

- prefactor :math:`p` is a float
- observable `\hat{O}` is one of :class:`~.Hermitian`,
:class:`~.SparseHamiltonian`, or a tensor product
of Pauli words.
"""
if isinstance(gen, (qml.Hermitian, qml.SparseHamiltonian)):
obs = gen
prefactor = 1.0

elif isinstance(gen, qml.operation.Observable):
# convert to a qml.Hamiltonian
gen = 1.0 * gen

if len(gen.ops) == 1:
# case where the Hamiltonian is a single Pauli word
obs = gen.ops[0]
prefactor = gen.coeffs[0]
else:
obs = gen
prefactor = 1.0

if op.inverse:
prefactor *= -1.0

return obs, prefactor


@qml.op_transform
def generator(op, format="prefactor"):
r"""Returns the generator of an operation.

Args:
op (.Operator or Callable): A single operator, or a function that
applies a single quantum operation.
format (str): The format to return the generator in. Must be one of ``'prefactor'``,
``'observable'``, or ``'hamiltonian'``. See below for more details.

Returns:
.Observable or tuple[float, .Observable]: The returned generator, with format/type
dependent on the ``format`` argument.

* ``"prefactor"``: Return the generator as ```(obs, prefactor)`` (representing
:math:`G=p \hat{O}`), where

- observable `\hat{O}` is one of :class:`~.Hermitian`,
:class:`~.SparseHamiltonian`, or a tensor product
of Pauli words.
- prefactor :math:`p` is a float

The prefactor will in most cases be :math:`\pm 1.0`, unless the generator is a single Pauli
word, in which case the prefactor is the coefficient of the Pauli word.

* ``"observable"``: Return the generator as a single observable as directly defined
by ``op``. Returned generators may be any type of observable, including
:class:`~.Hermitian`, :class:`~.Tensor`,
:class:`~.SparseHamiltonian`, or :class:`~.Hamiltonian`.

* ``"hamiltonian"``: Similar to ``"observable"``, however the returned observable
will always be converted into :class:`~.Hamiltonian` regardless of how ``op``
encodes the generator.

**Example**

Given an operation, ``qml.generator`` returns the generator representation:

>>> op = qml.CRX(0.6, wires=[0, 1])
>>> qml.generator(op)
(Projector([1], wires=[0]) @ PauliX(wires=[1]), -0.5)

It can also be used in a functional form:

>>> qml.generator(qml.CRX)(0.6, wires=[0, 1])
(Projector([1], wires=[0]) @ PauliX(wires=[1]), -0.5)

By default, ``generator`` will return the generator in the format of ``(obs, prefactor)``,
corresponding to :math:`G=p \hat{O}`, where the observable :math:`\hat{O}` will
always be given in tensor product form, or as a dense/sparse matrix.

By using the ``format`` argument, the returned generator representation can
be altered:

>>> op = qml.RX(0.2, wires=0)
>>> qml.generator(op, format="prefactor") # output will always be (prefactor, obs)
(Projector([1], wires=[0]), 1.0)
>>> qml.generator(op, format="hamiltonian") # output will always be a Hamiltonian
<Hamiltonian: terms=1, wires=[0]>
>>> qml.generator(op, format="observable") # ouput will be a simplified obs where possible
Projector([1], wires=[0])
"""
if op.num_params != 1:
raise ValueError(f"Operation {op.name} is not written in terms of a single parameter")
josh146 marked this conversation as resolved.
Show resolved Hide resolved

gen = op.generator()

if not isinstance(gen, qml.operation.Observable):
raise qml.QuantumFunctionError(
f"Generator {gen.name} of operation {op.name} is not an observable"
)

if format == "prefactor":
return _generator_prefactor(gen, op)

if format == "hamiltonian":
return _generator_hamiltonian(gen, op)

if format == "observable":
return _generator_observable(gen, op)

raise ValueError("format must be one of ('prefactor', 'hamiltonian', 'observable')")
4 changes: 2 additions & 2 deletions pennylane/tape/reversible.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,10 @@ def reversible_diff(self, idx, params, **options):

if op.name == "Rot":
decomp = op.decomposition()
generator, multiplier = qml.utils.get_generator(decomp[p_idx])
generator, multiplier = qml.generator(decomp[p_idx])
between_ops = decomp[p_idx + 1 :] + between_ops
else:
generator, multiplier = qml.utils.get_generator(op)
generator, multiplier = qml.generator(op)

# construct circuit to compute differentiated state
between_ops_inverse = [copy.copy(op) for op in between_ops[::-1]]
Expand Down
6 changes: 4 additions & 2 deletions pennylane/transforms/adjoint_metric_tensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ def _adjoint_metric_tensor_tape(tape, device):
psi = _apply_operations(psi, group_after_trainable_op[-1], device)

for j, outer_op in enumerate(trainable_operations):
generator_1, prefactor_1 = qml.utils.get_generator(outer_op, return_matrix=True)
generator_1, prefactor_1 = qml.generator(outer_op)
generator_1 = qml.matrix(generator_1)

# the state vector phi is missing a factor of 1j * prefactor_1
phi = device._apply_unitary(
Expand Down Expand Up @@ -223,7 +224,8 @@ def _adjoint_metric_tensor_tape(tape, device):
lam = _apply_operations(lam, group_after_trainable_op[i], device, invert=True)
inner_op = trainable_operations[i]
# extract and apply G_i
generator_2, prefactor_2 = qml.utils.get_generator(inner_op, return_matrix=True)
generator_2, prefactor_2 = qml.generator(inner_op)
generator_2 = qml.matrix(generator_2)
# this state vector is missing a factor of 1j * prefactor_2
mu = device._apply_unitary(lam, qml.math.convert_like(generator_2, lam), inner_op.wires)
phi_real = qml.math.reshape(qml.math.real(phi), (dim,))
Expand Down
6 changes: 3 additions & 3 deletions pennylane/transforms/metric_tensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ def _metric_tensor_cov_matrix(tape, diag_approx):

# for each operation in the layer, get the generator
for op in curr_ops:
obs, s = qml.utils.get_generator(op)
obs, s = qml.generator(op)
obs_list[-1].append(obs)
coeffs_list[-1].append(s)

Expand Down Expand Up @@ -460,8 +460,8 @@ def _get_gen_op(op, allow_nonunitary, aux_wire):

except KeyError as e:
if allow_nonunitary:
gen, coeff = qml.utils.get_generator(op, return_matrix=True)
return qml.ControlledQubitUnitary(coeff * gen, control_wires=aux_wire, wires=op.wires)
mat = qml.matrix(qml.generator(op, format="observable"))
return qml.ControlledQubitUnitary(mat, control_wires=aux_wire, wires=op.wires)

raise ValueError(
f"Generator for operation {op} not known and non-unitary operations "
Expand Down
16 changes: 13 additions & 3 deletions pennylane/transforms/op_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,13 @@ def fn(self, obj, *args, **kwargs):
# the tape transform function if defined
return self.tape_fn(obj.expand(), *args, **kwargs)

except (AttributeError, OperationTransformError):
# if obj.expand() does not exist, or the tape transform
# function does not exist, simply raise the original exception
except (
AttributeError,
qml.operation.OperatorPropertyUndefined,
OperationTransformError,
):
# if obj.expand() does not exist, a required operation property was not found,
# or the tape transform function does not exist, simply raise the original exception
raise e1 from None

def tape_fn(self, obj, *args, **kwargs):
Expand Down Expand Up @@ -423,6 +427,12 @@ def wrapper(*args, **kwargs):
nonlocal wire_order
tape, verified_wire_order = self._make_tape(obj, wire_order, *args, **kwargs)

# HOTFIX: some operator transforms return a tape containing
# a single transformed operator. As a result, for now we need
# to treat a tape with a single operation as a single operation.
if len(getattr(tape, "operations", [])) == 1 and self._tape_fn is None:
tape = tape.operations[0]

josh146 marked this conversation as resolved.
Show resolved Hide resolved
if wire_order is not None or (
"wire_order" in self._sig and isinstance(obj, qml.QNode)
):
Expand Down
Loading