qml.pulse¶
Pulse programming is used in a variety of quantum systems for low-level control of quantum operations. A time-dependent electromagnetic field tuned to the characteristic energies is applied, leading to a time-dependent Hamiltonian interaction \(H(t)\). Driving the system with such an electromagnetic field for a fixed time window is a pulse program. This pulse program can be tuned to implement the higher level gates used for quantum computation.
The pulse
module provides functions and classes used to simulate pulse-level control of quantum
systems.
It contains a ParametrizedHamiltonian
and ParametrizedEvolution
class for
describing time-dependent Hamiltonian interactions. The pulse
module also includes several convenience
functions for defining pulses.
The pulse
module is written for jax
and will not work with other machine learning frameworks
typically encountered in PennyLane. It requires separate installation, see
jax.readthedocs.io.
For a demonstration of the basic pulse functionality in PennyLane and running a ctrl-VQE example, see our demo on differentiable pulse programming.
Overview¶
Time evolution classes¶
|
Callable object holding the information representing a parametrized Hamiltonian. |
|
Parametrized evolution gate, created by passing a |
Convenience Functions¶
|
Returns the given |
|
Takes a time span and returns a callable for creating a function that is piece-wise constant in time. |
|
Decorates a smooth function, creating a piece-wise constant function that approximates it. |
|
Takes a scalar or a scalar-valued function, x, and applies a rectangular window to it, such that the returned function is x inside the window and 0 outside it. |
Hardware Compatible Hamiltonians¶
|
Returns a |
|
Returns a |
|
Returns a |
|
Returns a |
Creating a parametrized Hamiltonian¶
The pulse
module provides a framework to create a time-dependent Hamiltonian of the form
with constant operators \(H_j\) and scalar functions \(f_j(v_j, t)\) that may depend on parameters \(p\) and time \(t\).
Defining a ParametrizedHamiltonian
requires coefficients and operators, where some of the coefficients
are callables. The callables defining the parametrized coefficients must have the call signature (p, t)
, where p
can be a float
,
list
or jnp.array
. These functions should be defined using jax.numpy
rather than numpy
where relevant.
import pennylane as qml
from jax import numpy as jnp
# defining the coefficients fj(p, t) for the two parametrized terms
f1 = lambda p, t: p * jnp.sin(t) * (t - 1)
f2 = lambda p, t: p[0] * jnp.cos(p[1]* t ** 2)
# defining the operations for the three terms in the Hamiltonian
XX = qml.X(0) @ qml.X(1)
YY = qml.Y(0) @ qml.Y(1)
ZZ = qml.Z(0) @ qml.Z(1)
There are two ways to construct a ParametrizedHamiltonian
from the coefficients
and operators:
# Option 1
H1 = 2 * XX + f1 * YY + f2 * ZZ
# Option 2
coeffs = [2, f1, f2]
ops = [XX, YY, ZZ]
H2 = qml.dot(coeffs, ops)
Warning
When initializing a 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 * t for _ in range(3)]
.
Be careful when defining coefficients using lambda functions within a list comprehension. Avoid
doing coeffs = [lambda p, t: p * t**i for i in range(3)]
, which will only use the final index i=2
in the lambda
and will thus behave as coeffs = [(lambda p, t: p * t**2)] * 3
.
Instead, use coeffs = [lambda p, t, power=i: p * t**power for i in range(3)]
The ParametrizedHamiltonian
is a callable, and can return an Operator
if passed a set of
parameters and a time at which to evaluate the coefficients \(f_j\).
>>> H1
(
2 * X(0) @ X(1)
+ <lambda>(params_0, t) * Y(0) @ Y(1)
+ <lambda>(params_1, t) * Z(0) @ Z(1)
)
>>> params = [1.2, [2.3, 3.4]] # f1 takes a single parameter, f2 takes 2
>>> H1(params, t=0.5)
(
2 * (X(0) @ X(1))
+ -0.2876553231625218 * (Y(0) @ Y(1))
+ 1.517961235535459 * (Z(0) @ Z(1))
)
When passing parameters, ensure that the order of the coefficient functions and the order of the parameters match.
When initializing a ParametrizedHamiltonian
, terms defined with fixed coefficients have to come
before parametrized terms to prevent discrepancy in the wire order.
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.
ParametrizedEvolution¶
During a pulse program spanning time \((t_0, t_1)\), the state evolves according to the time-dependent Schrodinger equation
realizing a unitary evolution \(U(t_0, t_1)\) of the input state, i.e.
A ParametrizedEvolution
is this solution \(U(t_0, t_1)\) to the time-dependent
Schrödinger equation for a ParametrizedHamiltonian
.
The ParametrizedEvolution
class uses a numerical ordinary differential equation
solver (see jax.experimental.ode). It
can be created using the evolve()
function:
from jax import numpy as jnp
f1 = lambda p, t: p * jnp.sin(t) * (t - 1)
H = 2 * qml.X(0) + f1 * qml.Y(1)
ev = qml.evolve(H)
>>> ev
ParametrizedEvolution(wires=[0, 1])
The initial ParametrizedEvolution
does not have parameters defined, and so will
not have a matrix defined. To obtain an Operator
with a matrix, we have to pass
parameters and a time interval:
>>> ev([1.2], t=[0, 4]).matrix()
Array([[-0.14115842+0.j , 0.03528605+0.j ,
0. -0.95982337j, 0. +0.23993255j],
[-0.03528605+0.j , -0.14115842+0.j ,
0. -0.23993255j, 0. -0.95982337j],
[ 0. -0.95982337j, 0. +0.23993255j,
-0.14115842+0.j , 0.03528605+0.j ],
[ 0. -0.23993255j, 0. -0.95982337j,
-0.03528605+0.j , -0.14115842+0.j ]], dtype=complex64)
The parameters can be updated by calling the ParametrizedEvolution
again with different inputs.
Additional options with regards to how the matrix is calculated can be passed to the ParametrizedEvolution
along with the parameters, as keyword arguments:
>>> qml.evolve(H)(params=[1.2], t=[0, 4], atol=1e-6, mxstep=1)
ParametrizedEvolution(Array(1.2, dtype=float32, weak_type=True), wires=[0, 1])
The available keyword arguments can be found in in ParametrizedEvolution
. If not specified, they
will default to predetermined values.
Using qml.evolve in a QNode¶
The ParametrizedEvolution
can be implemented in a QNode. We will evolve the
following ParametrizedHamiltonian
:
from jax import numpy as jnp
f1 = lambda p, t: jnp.sin(p * t)
H = f1 * qml.Y(0)
Now we can execute the evolution of this Hamiltonian in a QNode and compute its gradient:
import jax
jax.config.update("jax_enable_x64", True)
dev = qml.device("default.qubit", wires=1)
@jax.jit
@qml.qnode(dev, interface="jax")
def circuit(params):
qml.evolve(H)(params, t=[0, 10])
return qml.expval(qml.Z(0))
>>> params = [1.2]
>>> circuit(params)
Array(0.96632722, dtype=float64)
>>> jax.grad(circuit)(params)
[Array(2.35694829, dtype=float64)]
We can use the decorator jax.jit
to compile this execution just-in-time. This means the first execution
will typically take a little longer with the benefit that all following executions will be significantly faster.
JIT-compiling is optional, and one can remove the decorator when only single executions are of interest. See the
jax
docs on jitting for more information.
Warning
To find the simultaneous evolution of the two operators, it is important that they are included
in the same evolve()
. For two non-commuting ParametrizedHamiltonian
’s, applying
qml.evolve(H1)(params, t=[0, 10])
followed by qml.evolve(H2)(params, t=[0, 10])
will not
apply the two pulses simultaneously, despite the overlapping time window. Instead, they will be evolved
over the same timespan, but without taking into account how the evolution of H1
affects H2
.
See Usage Details of ParametrizedEvolution
for a detailed example.