Source code for pennylane.pulse.parametrized_hamiltonian
# Copyright 2018-2023 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 submodule contains the ParametrizedHamiltonian class
"""
from copy import copy
import pennylane as qml
from pennylane.operation import Operator
from pennylane.ops import Sum
from pennylane.typing import TensorLike
from pennylane.wires import Wires
# pylint: disable= too-many-instance-attributes
[docs]class ParametrizedHamiltonian:
r"""Callable object holding the information representing a parametrized Hamiltonian.
The Hamiltonian can be represented as a linear combination of other operators, e.g.,
.. math::
H(\{v_j\}, t) = H_\text{drift} + \sum_j f_j(v_j, t) H_j
where the :math:`\{v_j\}` are trainable parameters for each scalar-valued parametrization :math:`f_j`, and t is time.
Args:
coeffs (Union[float, callable]): coefficients of the Hamiltonian expression, which may be
constants or parametrized functions. All functions passed as ``coeffs`` must have two
arguments, the first one being the trainable parameters and the second one being time.
observables (Iterable[Operator]): observables in the Hamiltonian expression, of same
length as ``coeffs``
A ``ParametrizedHamiltonian`` is a callable with the fixed signature ``H(params, t)``,
with ``params`` being an iterable where each element corresponds to the parameters of each
scalar-valued function of the Hamiltonian.
Calling the ``ParametrizedHamiltonian`` returns an :class:`~.Operator` representing an instance of the
Hamiltonian with the specified parameter values.
.. seealso::
:func:`~.pennylane.evolve`, :class:`~.ParametrizedEvolution`
.. note::
The ``ParametrizedHamiltonian`` must be Hermitian at all times. This is not explicitly
checked; ensuring a correctly defined Hamiltonian is the responsibility of the user.
**Example**
A ``ParametrizedHamiltonian`` can be created using :func:`~.pennylane.dot`, by passing a list of coefficients,
as well as a list of corresponding observables. Each coefficient function must take two arguments, the first one
being the trainable parameters and the second one being time, though it need not use them both.
.. code-block:: python3
f1 = lambda p, t: p[0] * jnp.sin(p[1] * t)
f2 = lambda p, t: p * t
coeffs = [2., f1, f2]
observables = [qml.X(0), qml.Y(0), qml.Z(0)]
H = qml.dot(coeffs, observables)
The resulting object can be passed parameters, and will return an :class:`~.Operator` representing the
``ParametrizedHamiltonian`` with the specified parameters. Note that parameters must be passed in the order
the functions were passed in creating the ``ParametrizedHamiltonian``:
.. code-block:: python3
p1 = jnp.array([1., 1.])
p2 = 1.
params = [p1, p2] # p1 is passed to f1, and p2 to f2
>>> H(params, t=1.)
(
2.0 * X(0)
+ 0.8414709848078965 * Y(0)
+ 1.0 * Z(0)
)
.. note::
To be able to compute the time evolution of the Hamiltonian with :func:`~.pennylane.evolve`,
these coefficient functions should be defined using ``jax.numpy`` rather than ``numpy``.
We can also access the fixed and parametrized terms of the ``ParametrizedHamiltonian``.
The fixed term is an :class:`~.Operator`, while the parametrized term must be initialized with concrete
parameters to obtain an :class:`~.Operator`:
>>> H.H_fixed()
2.0 * X(0)
>>> H.H_parametrized([[1.2, 2.3], 4.5], 0.5)
1.095316728312625 * Y(0) + 2.25 * Z(0)
.. details::
:title: Usage Details
An alternative method for creating a ``ParametrizedHamiltonian`` is to multiply operators and callable
coefficients:
.. code-block:: python3
def f1(p, t):
return jnp.sin(p[0] * t**2) + p[1]
def f2(p, t):
return p * jnp.cos(t)
H = 2 * qml.X(0) + f1 * qml.Y(0) + f2 * qml.Z(0)
.. note::
Whichever method is used for initializing a :class:`~.ParametrizedHamiltonian`, the terms defined with fixed
coefficients should come before parametrized terms to prevent discrepancy in the wire order.
.. note::
The parameters used in the ``ParametrizedHamiltonian`` call should have the same order
as the functions used to define this Hamiltonian. For example, we could call the above
Hamiltonian using the following parameters:
>>> params = [[4.6, 2.3], 1.2]
>>> H(params, t=0.5)
(
2 * X(0)
+ 3.212763940260521 * Y(0)
+ 1.0530990742684472 * Z(0)
)
Internally we are computing ``f1([4.6, 2.3], 0.5)`` and ``f2(1.2, 0.5)``.
Parametrized coefficients can be any callable that takes ``(p, t)`` and returns a scalar. It is not a
requirement that both ``p`` and ``t`` be used in the callable: for example, the convenience function
:func:`~pulse.constant` takes ``(p, t)`` and returns ``p``.
.. warning::
When initializing a :class:`~.ParametrizedHamiltonian` via a list of parametrized coefficients, it
is possible to create a list of multiple coefficients of the same form iteratively using lambda
functions, i.e.
``coeffs = [lambda p, t: p for _ in range(3)]``.
Do **not**, however, define the function as dependent on the value that is iterated over. That is, it is not
possible to define
``coeffs = [lambda p, t: p * t**i for i in range(3)]``
to create a list
``coeffs = [(lambda p, t: p), (lambda p, t: p * t), (lambda p, t: p * t**2)]``.
The value of ``i`` when
creating the lambda functions is set to be the final value in the iteration, such that this will
produce three identical functions
``coeffs = [(lambda p, t: p * t**2)] * 3``.
We can visualize the behaviour in time of the parametrized coefficients for a given set of parameters. Here
we look at the Hamiltonian created above:
.. code-block:: python
import matplotlib.pyplot as plt
times = jnp.linspace(0., 5., 1000)
fs = tuple(c for c in H.coeffs if callable(c))
params = [[4.6, 2.3], 1.2]
fig, axs = plt.subplots(nrows=len(fs))
for n, f in enumerate(fs):
ax = axs[n]
ax.plot(times, f(params[n], times), label=f"p={params[n]}")
ax.set_ylabel(f"f{n}")
ax.legend(loc="upper left")
ax.set_xlabel("Time")
axs[0].set_title(f"H parametrized coefficients")
plt.tight_layout()
plt.show()
.. figure:: ../../_static/pulse/parametrized_coefficients_example.png
:align: center
:width: 60%
:target: javascript:void(0);
It is possible to add two instance of ``ParametrizedHamiltonian`` together. The resulting
``ParametrizedHamiltonian`` takes a list of parameters that is a concatenation of the initial
two Hamiltonian parameters:
.. code-block:: python3
coeffs = [lambda p, t: jnp.sin(p*t) for _ in range(2)]
ops = [qml.X(0), qml.Y(1)]
H1 = qml.dot(coeffs, ops)
def f1(p, t): return t + p
def f2(p, t): return p[0] * jnp.sin(p[1] * t**2)
H2 = f1 * qml.Y(0) + f2 * qml.X(1)
params1 = [2., 3.]
params2 = [4., [5., 6.]]
>>> H3 = H2 + H1
>>> H3([4., [5., 6.], 2., 3.], t=1)
(
5.0 * Y(0)
+ -1.3970774909946293 * X(1)
+ 0.9092974268256817 * X(0)
+ 0.1411200080598672 * Y(1)
)
"""
def __init__(self, coeffs, observables):
if len(coeffs) != len(observables):
raise ValueError(
"Could not create valid Hamiltonian; "
"number of coefficients and operators does not match."
f"Got len(coeffs) = {len(coeffs)} and len(observables) = {len(observables)}."
)
self.coeffs_fixed = []
self.coeffs_parametrized = []
self.ops_fixed = []
self.ops_parametrized = []
for coeff, obs in zip(coeffs, observables):
if callable(coeff):
self.coeffs_parametrized.append(coeff)
self.ops_parametrized.append(obs)
else:
self.coeffs_fixed.append(coeff)
self.ops_fixed.append(obs)
self.wires = Wires.all_wires(
[op.wires for op in self.ops_fixed] + [op.wires for op in self.ops_parametrized]
)
def __call__(self, params, t):
if len(params) != len(self.coeffs_parametrized):
raise ValueError(
"The length of the params argument and the number of scalar-valued functions "
f"must be the same. Received len(params) = {len(params)} parameters but "
f"expected {len(self.coeffs_parametrized)} parameters."
)
H_fixed = self.H_fixed()
H_param = self.H_parametrized(params, t)
if H_param == 0:
return H_fixed
if H_fixed == 0:
return H_param
return qml.sum(self.H_fixed(), self.H_parametrized(params, t))
def __repr__(self):
terms = []
for coeff, op in zip(self.coeffs_fixed, self.ops_fixed):
term = f"{coeff} * {op}"
terms.append(term)
for i, (coeff, op) in enumerate(zip(self.coeffs_parametrized, self.ops_parametrized)):
op_repr = f"({op})" if isinstance(op, Sum) else str(op)
named_coeff = coeff if callable(coeff) and hasattr(coeff, "__name__") else type(coeff)
term = f"{named_coeff.__name__}(params_{i}, t) * {op_repr}"
terms.append(term)
res = "\n + ".join(terms)
return f"(\n {res}\n)"
[docs] def map_wires(self, wire_map):
"""Returns a copy of the current ParametrizedHamiltonian with its wires changed according
to the given wire map.
Args:
wire_map (dict): dictionary containing the old wires as keys and the new wires as values
Returns:
.ParametrizedHamiltonian: A new instance with mapped wires
"""
new_ph = copy(self)
new_ph.ops_parametrized = [op.map_wires(wire_map) for op in self.ops_parametrized]
new_ph.ops_fixed = [op.map_wires(wire_map) for op in self.ops_fixed]
new_ph.wires = Wires.all_wires(
[op.wires for op in new_ph.ops_fixed] + [op.wires for op in new_ph.ops_parametrized]
)
return new_ph
[docs] def H_fixed(self):
"""The fixed term(s) of the ``ParametrizedHamiltonian``. Returns a ``Sum`` operator of ``SProd``
operators (or a single ``SProd`` operator in the event that there is only one term in ``H_fixed``).
"""
if self.coeffs_fixed:
return sum(qml.s_prod(c, o) for c, o in zip(self.coeffs_fixed, self.ops_fixed))
return 0
[docs] def H_parametrized(self, params, t):
"""The parametrized terms of the Hamiltonian for the specified parameters and time.
Args:
params(tensor_like): the parameters values used to evaluate the operators
t(float): the time at which the operator is evaluated
Returns:
Operator: a ``Sum`` of ``SProd`` operators (or a single
``SProd`` operator in the event that there is only one term in ``H_parametrized``).
"""
coeffs = [f(param, t) for f, param in zip(self.coeffs_parametrized, params)]
return sum(qml.s_prod(c, o) for c, o in zip(coeffs, self.ops_parametrized)) if coeffs else 0
@property
def coeffs(self):
"""Return the coefficients defining the ``ParametrizedHamiltonian``, including the unevaluated
functions for the parametrized terms.
Returns:
Iterable[float, Callable]): coefficients in the Hamiltonian expression
"""
return self.coeffs_fixed + self.coeffs_parametrized
@property
def ops(self):
"""Return the operators defining the ``ParametrizedHamiltonian``.
Returns:
Iterable[Operator]: observables in the Hamiltonian expression
"""
return self.ops_fixed + self.ops_parametrized
def __add__(self, H):
r"""The addition operation between a ``ParametrizedHamiltonian`` and an ``Operator``
or ``ParametrizedHamiltonian``."""
ops = self.ops.copy()
coeffs = self.coeffs.copy()
if isinstance(H, (qml.ops.Hamiltonian, qml.ops.LinearCombination, ParametrizedHamiltonian)):
# if Hamiltonian, coeffs array must be converted to list
new_coeffs = coeffs + list(H.coeffs.copy())
new_ops = ops + H.ops.copy()
return ParametrizedHamiltonian(new_coeffs, new_ops)
if isinstance(H, qml.ops.SProd): # pylint: disable=no-member
new_coeffs = coeffs + [H.scalar]
new_ops = ops + [H.base]
return ParametrizedHamiltonian(new_coeffs, new_ops)
if isinstance(H, Operator):
new_coeffs = coeffs + [1]
new_ops = ops + [H]
return ParametrizedHamiltonian(new_coeffs, new_ops)
return NotImplemented
def __radd__(self, H):
r"""The addition operation between a ``ParametrizedHamiltonian`` and an ``Operator``
or ``ParametrizedHamiltonian``."""
ops = self.ops.copy()
coeffs = self.coeffs.copy()
if isinstance(H, (qml.ops.Hamiltonian, qml.ops.LinearCombination, ParametrizedHamiltonian)):
# if Hamiltonian, coeffs array must be converted to list
new_coeffs = list(H.coeffs.copy()) + coeffs
new_ops = H.ops.copy() + ops
return ParametrizedHamiltonian(new_coeffs, new_ops)
if isinstance(H, qml.ops.SProd): # pylint: disable=no-member
new_coeffs = [H.scalar] + coeffs
new_ops = [H.base] + ops
return ParametrizedHamiltonian(new_coeffs, new_ops)
if isinstance(H, Operator):
new_coeffs = [1] + coeffs
new_ops = [H] + ops
return ParametrizedHamiltonian(new_coeffs, new_ops)
return NotImplemented
def __mul__(self, other):
ops = self.ops.copy()
coeffs_fixed = self.coeffs_fixed.copy()
coeffs_parametrized = self.coeffs_parametrized.copy()
if isinstance(other, TensorLike) and qml.math.ndim(other) == 0:
coeffs_fixed = [other * c for c in coeffs_fixed]
coeffs_parametrized = [
lambda p, t, new_c=c: other * new_c(p, t) for c in coeffs_parametrized
]
return ParametrizedHamiltonian(coeffs_fixed + coeffs_parametrized, ops)
return NotImplemented
__rmul__ = __mul__
_modules/pennylane/pulse/parametrized_hamiltonian
Download Python script
Download Notebook
View on GitHub