Skip to content

Commit

Permalink
Add qml.generator(op) function (#2256)
Browse files Browse the repository at this point in the history
* Add operator transform functionality

* Add matrix function:

* add tests

* add tests

* add tests

* add tests

* add tests

* add tests

* add tests

* more docs

* more

* add tests

* Apply suggestions from code review

Co-authored-by: Maria Schuld <mariaschuld@gmail.com>

* Add qml.eigval() function

* add test

* finish adding tests

* finish adding tests

* Add qml.generator(op) function

* fixes

* more tests

* more tests

* fix

* more tests

* Apply suggestions from code review

Co-authored-by: Maria Schuld <mariaschuld@gmail.com>

* Apply suggestions from code review

Co-authored-by: Maria Schuld <mariaschuld@gmail.com>

* Update tests/ops/functions/test_eigvals.py

* Apply suggestions from code review

Co-authored-by: Maria Schuld <mariaschuld@gmail.com>

* update deprecation warning

* Update tests/ops/functions/test_eigvals.py

* update deprecation warning

* add another test

* suggested changes

* black

* black

* Apply suggestions from code review

Co-authored-by: David Wierichs <davidwierichs@gmail.com>

* changelog

* more

* bugfix

* suggested changes

* change parameter-frequency logic

* Apply suggestions from code review

Co-authored-by: Maria Schuld <mariaschuld@gmail.com>

* fix

Co-authored-by: Maria Schuld <mariaschuld@gmail.com>
Co-authored-by: David Wierichs <davidwierichs@gmail.com>
  • Loading branch information
3 people authored Mar 3, 2022
1 parent d1a03a9 commit 3d36407
Show file tree
Hide file tree
Showing 15 changed files with 600 additions and 110 deletions.
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"))

# 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)

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")

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]

if wire_order is not None or (
"wire_order" in self._sig and isinstance(obj, qml.QNode)
):
Expand Down
Loading

0 comments on commit 3d36407

Please sign in to comment.