PennyLaneAI / pennylane

@@ -0,0 +1,693 @@
Loading
1 +
# Copyright 2018-2021 Xanadu Quantum Technologies Inc.
2 +
3 +
# Licensed under the Apache License, Version 2.0 (the "License");
4 +
# you may not use this file except in compliance with the License.
5 +
# You may obtain a copy of the License at
6 +
7 +
#     http://www.apache.org/licenses/LICENSE-2.0
8 +
9 +
# Unless required by applicable law or agreed to in writing, software
10 +
# distributed under the License is distributed on an "AS IS" BASIS,
11 +
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +
# See the License for the specific language governing permissions and
13 +
# limitations under the License.
14 +
"""
15 +
This module contains functions for computing the parameter-shift gradient
16 +
of a CV-based quantum tape.
17 +
"""
18 +
# pylint: disable=protected-access,too-many-arguments,too-many-statements,too-many-branches
19 +
import itertools
20 +
import warnings
21 +
22 +
import numpy as np
23 +
24 +
import pennylane as qml
25 +
26 +
from .finite_difference import finite_diff, generate_shifted_tapes
27 +
from .parameter_shift import expval_param_shift, _get_operation_recipe, _process_gradient_recipe
28 +
29 +
30 +
def _grad_method(tape, idx):
31 +
    """Determine the best CV parameter-shift gradient recipe for a given
32 +
    parameter index of a tape.
33 +
34 +
    Args:
35 +
        tape (.QuantumTape): input tape
36 +
        idx (int): positive integer corresponding to the parameter location
37 +
            on the tape to inspect
38 +
39 +
    Returns:
40 +
        str: a string containing either ``"A"`` (for first-order analytic method),
41 +
            ``"A2"`` (second-order analytic method), ``"F"`` (finite differences),
42 +
            or ``"0"`` (constant parameter).
43 +
    """
44 +
45 +
    op = tape._par_info[idx]["op"]
46 +
47 +
    if op.grad_method in (None, "F"):
48 +
        return op.grad_method
49 +
50 +
    if op.grad_method != "A":
51 +
        raise ValueError(f"Operation {op} has unknown gradient method {op.grad_method}")
52 +
53 +
    # Operation supports the CV parameter-shift rule.
54 +
    # Create an empty list to store the 'best' partial derivative method
55 +
    # for each observable
56 +
    best = []
57 +
58 +
    for m in tape.measurements:
59 +
60 +
        if (m.return_type is qml.operation.Probability) or (m.obs.ev_order not in (1, 2)):
61 +
            # Higher-order observables (including probability) only support finite differences.
62 +
            best.append("F")
63 +
            continue
64 +
65 +
        # get the set of operations betweens the operation and the observable
66 +
        ops_between = tape.graph.nodes_between(op, m.obs)
67 +
68 +
        if not ops_between:
69 +
            # if there is no path between the operation and the observable,
70 +
            # the operator has a zero gradient.
71 +
            best.append("0")
72 +
            continue
73 +
74 +
        # For parameter-shift compatible CV gates, we need to check both the
75 +
        # intervening gates, and the type of the observable.
76 +
        best_method = "A"
77 +
78 +
        if any(not k.supports_heisenberg for k in ops_between):
79 +
            # non-Gaussian operators present in-between the operation
80 +
            # and the observable. Must fallback to numeric differentiation.
81 +
            best_method = "F"
82 +
83 +
        elif m.obs.ev_order == 2:
84 +
85 +
            if m.return_type is qml.operation.Expectation:
86 +
                # If the observable is second-order, we must use the second-order
87 +
                # CV parameter shift rule
88 +
                best_method = "A2"
89 +
90 +
            elif m.return_type is qml.operation.Variance:
91 +
                # we only support analytic variance gradients for
92 +
                # first-order observables
93 +
                best_method = "F"
94 +
95 +
        best.append(best_method)
96 +
97 +
    if all(k == "0" for k in best):
98 +
        # if the operation is independent of *all* observables
99 +
        # in the circuit, the gradient will be 0
100 +
        return "0"
101 +
102 +
    if "F" in best:
103 +
        # one non-analytic observable path makes the whole operation
104 +
        # gradient method fallback to finite-difference
105 +
        return "F"
106 +
107 +
    if "A2" in best:
108 +
        # one second-order observable makes the whole operation gradient
109 +
        # require the second-order parameter-shift rule
110 +
        return "A2"
111 +
112 +
    return "A"
113 +
114 +
115 +
def _gradient_analysis(tape):
116 +
    """Update the parameter information dictionary of the tape with
117 +
    gradient information of each parameter."""
118 +
119 +
    if getattr(tape, "_gradient_fn", None) is param_shift_cv:
120 +
        # gradient analysis has already been performed on this tape
121 +
        return
122 +
123 +
    tape._gradient_fn = param_shift_cv
124 +
125 +
    for idx, info in tape._par_info.items():
126 +
        info["grad_method"] = _grad_method(tape, idx)
127 +
128 +
129 +
def _transform_observable(obs, Z, device_wires):
130 +
    """Apply a Gaussian linear transformation to an observable.
131 +
132 +
    Args:
133 +
        obs (.Observable): observable to transform
134 +
        Z (array[float]): Heisenberg picture representation of the linear transformation
135 +
        device_wires (.Wires): wires on the device the transformed observable is to be
136 +
            measured on
137 +
138 +
    Returns:
139 +
        .Observable: the transformed observable
140 +
    """
141 +
    # Get the Heisenberg representation of the observable
142 +
    # in the position/momentum basis. The returned matrix/vector
143 +
    # will have been expanded to act on the entire device.
144 +
    if obs.ev_order > 2:
145 +
        raise NotImplementedError("Transforming observables of order > 2 not implemented.")
146 +
147 +
    A = obs.heisenberg_obs(device_wires)
148 +
149 +
    if A.ndim != obs.ev_order:
150 +
        raise ValueError(
151 +
            "Mismatch between the polynomial order of observable and its Heisenberg representation"
152 +
        )
153 +
154 +
    # transform the observable by the linear transformation Z
155 +
    A = A @ Z
156 +
157 +
    if A.ndim == 2:
158 +
        A = A + A.T
159 +
160 +
    # TODO: if the A matrix corresponds to a known observable in PennyLane,
161 +
    # for example qml.X, qml.P, qml.NumberOperator, we should return that
162 +
    # instead. This will allow for greater device compatibility.
163 +
    return qml.PolyXP(A, wires=device_wires)
164 +
165 +
166 +
def var_param_shift(tape, dev_wires, argnum=None, shift=np.pi / 2, gradient_recipes=None, f0=None):
167 +
    r"""Partial derivative using the first-order or second-order parameter-shift rule of a tape
168 +
    consisting of a mixture of expectation values and variances of observables.
169 +
170 +
    Expectation values may be of first- or second-order observables,
171 +
    but variances can only be taken of first-order variables.
172 +
173 +
    .. warning::
174 +
175 +
        This method can only be executed on devices that support the
176 +
        :class:`~.PolyXP` observable.
177 +
178 +
    Args:
179 +
        tape (.QuantumTape): quantum tape to differentiate
180 +
        dev_wires (.Wires): wires on the device the parameter-shift method is computed on
181 +
        argnum (int or list[int] or None): Trainable parameter indices to differentiate
182 +
            with respect to. If not provided, the derivative with respect to all
183 +
            trainable indices are returned.
184 +
        shift (float): The shift value to use for the two-term parameter-shift formula.
185 +
            Only valid if the operation in question supports the two-term parameter-shift
186 +
            rule (that is, it has two distinct eigenvalues) and ``gradient_recipes``
187 +
            is ``None``.
188 +
        gradient_recipes (tuple(list[list[float]] or None)): List of gradient recipes
189 +
            for the parameter-shift method. One gradient recipe must be provided
190 +
            per trainable parameter.
191 +
        f0 (tensor_like[float] or None): Output of the evaluated input tape. If provided,
192 +
            and the gradient recipe contains an unshifted term, this value is used,
193 +
            saving a quantum evaluation.
194 +
195 +
    Returns:
196 +
        tuple[list[QuantumTape], function]: A tuple containing a
197 +
        list of generated tapes, in addition to a post-processing
198 +
        function to be applied to the evaluated tapes.
199 +
    """
200 +
    argnum = argnum or tape.trainable_params
201 +
202 +
    # Determine the locations of any variance measurements in the measurement queue.
203 +
    var_mask = [m.return_type is qml.operation.Variance for m in tape.measurements]
204 +
    var_idx = np.where(var_mask)[0]
205 +
206 +
    # Get <A>, the expectation value of the tape with unshifted parameters.
207 +
    expval_tape = tape.copy(copy_operations=True)
208 +
209 +
    # Convert all variance measurements on the tape into expectation values
210 +
    for i in var_idx:
211 +
        obs = expval_tape._measurements[i].obs
212 +
        expval_tape._measurements[i] = qml.measure.MeasurementProcess(
213 +
            qml.operation.Expectation, obs=obs
214 +
        )
215 +
216 +
    gradient_tapes = [expval_tape]
217 +
218 +
    # evaluate the analytic derivative of <A>
219 +
    pdA_tapes, pdA_fn = expval_param_shift(expval_tape, argnum, shift, gradient_recipes, f0)
220 +
    gradient_tapes.extend(pdA_tapes)
221 +
222 +
    # Store the number of first derivative tapes, so that we know
223 +
    # the number of results to post-process later.
224 +
    tape_boundary = len(pdA_tapes) + 1
225 +
    expval_sq_tape = tape.copy(copy_operations=True)
226 +
227 +
    for i in var_idx:
228 +
        # We need to calculate d<A^2>/dp; to do so, we replace the
229 +
        # observables A in the queue with A^2.
230 +
        obs = expval_sq_tape._measurements[i].obs
231 +
232 +
        # CV first-order observable
233 +
        # get the heisenberg representation
234 +
        # This will be a real 1D vector representing the
235 +
        # first-order observable in the basis [I, x, p]
236 +
        A = obs._heisenberg_rep(obs.parameters)
237 +
238 +
        # take the outer product of the heisenberg representation
239 +
        # with itself, to get a square symmetric matrix representing
240 +
        # the square of the observable
241 +
        obs = qml.PolyXP(np.outer(A, A), wires=obs.wires)
242 +
        expval_sq_tape._measurements[i] = qml.measure.MeasurementProcess(
243 +
            qml.operation.Expectation, obs=obs
244 +
        )
245 +
246 +
    # Non-involutory observables are present; the partial derivative of <A^2>
247 +
    # may be non-zero. Here, we calculate the analytic derivatives of the <A^2>
248 +
    # observables.
249 +
    pdA2_tapes, pdA2_fn = second_order_param_shift(
250 +
        expval_sq_tape, dev_wires, argnum, shift, gradient_recipes
251 +
    )
252 +
    gradient_tapes.extend(pdA2_tapes)
253 +
254 +
    def processing_fn(results):
255 +
        mask = qml.math.convert_like(qml.math.reshape(var_mask, [-1, 1]), results[0])
256 +
        f0 = qml.math.expand_dims(results[0], -1)
257 +
258 +
        pdA = pdA_fn(results[1:tape_boundary])
259 +
        pdA2 = pdA2_fn(results[tape_boundary:])
260 +
261 +
        # return d(var(A))/dp = d<A^2>/dp -2 * <A> * d<A>/dp for the variances (mask==True)
262 +
        # d<A>/dp for plain expectations (mask==False)
263 +
        return qml.math.where(mask, pdA2 - 2 * f0 * pdA, pdA)
264 +
265 +
    return gradient_tapes, processing_fn
266 +
267 +
268 +
def second_order_param_shift(tape, dev_wires, argnum=None, shift=np.pi / 2, gradient_recipes=None):
269 +
    r"""Generate the second-order CV parameter-shift tapes and postprocessing methods required
270 +
    to compute the gradient of a gate parameter with respect to an
271 +
    expectation value.
272 +
273 +
    .. note::
274 +
275 +
        The 2nd order method can handle also first-order observables, but
276 +
        1st order method may be more efficient unless it's really easy to
277 +
        experimentally measure arbitrary 2nd order observables.
278 +
279 +
    .. warning::
280 +
281 +
        The 2nd order method can only be executed on devices that support the
282 +
        :class:`~.PolyXP` observable.
283 +
284 +
    Args:
285 +
        tape (.QuantumTape): quantum tape to differentiate
286 +
        dev_wires (.Wires): wires on the device the parameter-shift method is computed on
287 +
        argnum (int or list[int] or None): Trainable parameter indices to differentiate
288 +
            with respect to. If not provided, the derivative with respect to all
289 +
            trainable indices are returned.
290 +
        shift (float): The shift value to use for the two-term parameter-shift formula.
291 +
            Only valid if the operation in question supports the two-term parameter-shift
292 +
            rule (that is, it has two distinct eigenvalues) and ``gradient_recipes``
293 +
            is ``None``.
294 +
        gradient_recipes (tuple(list[list[float]] or None)): List of gradient recipes
295 +
            for the parameter-shift method. One gradient recipe must be provided
296 +
            per trainable parameter.
297 +
298 +
    Returns:
299 +
        tuple[list[QuantumTape], function]: A tuple containing a
300 +
        list of generated tapes, in addition to a post-processing
301 +
        function to be applied to the evaluated tapes.
302 +
    """
303 +
    argnum = argnum or list(tape.trainable_params)
304 +
    gradient_recipes = gradient_recipes or [None] * len(argnum)
305 +
306 +
    gradient_tapes = []
307 +
    shapes = []
308 +
    obs_indices = []
309 +
    gradient_values = []
310 +
311 +
    for idx, _ in enumerate(tape.trainable_params):
312 +
        t_idx = list(tape.trainable_params)[idx]
313 +
        op = tape._par_info[t_idx]["op"]
314 +
315 +
        if idx not in argnum:
316 +
            # parameter has zero gradient
317 +
            shapes.append(0)
318 +
            obs_indices.append([])
319 +
            gradient_values.append([])
320 +
            continue
321 +
322 +
        shapes.append(1)
323 +
324 +
        # get the gradient recipe for the trainable parameter
325 +
        recipe = gradient_recipes[argnum.index(idx)]
326 +
        recipe = recipe or _get_operation_recipe(tape, idx, shift=shift)
327 +
        recipe = _process_gradient_recipe(recipe)
328 +
        coeffs, multipliers, shifts = recipe
329 +
330 +
        if len(shifts) != 2:
331 +
            # The 2nd order CV parameter-shift rule only accepts two-term shifts
332 +
            raise NotImplementedError(
333 +
                "Taking the analytic gradient for order-2 operators is "
334 +
                f"unsupported for operation {op} which has a "
335 +
                "gradient recipe of more than two terms."
336 +
            )
337 +
338 +
        shifted_tapes = generate_shifted_tapes(tape, idx, shifts, multipliers)
339 +
340 +
        # evaluate transformed observables at the original parameter point
341 +
        # first build the Heisenberg picture transformation matrix Z
342 +
        Z0 = op.heisenberg_tr(dev_wires, inverse=True)
343 +
        Z2 = shifted_tapes[0]._par_info[t_idx]["op"].heisenberg_tr(dev_wires)
344 +
        Z1 = shifted_tapes[1]._par_info[t_idx]["op"].heisenberg_tr(dev_wires)
345 +
346 +
        # derivative of the operation
347 +
        Z = Z2 * coeffs[0] + Z1 * coeffs[1]
348 +
        Z = Z @ Z0
349 +
350 +
        # conjugate Z with all the descendant operations
351 +
        B = np.eye(1 + 2 * len(dev_wires))
352 +
        B_inv = B.copy()
353 +
354 +
        succ = tape.graph.descendants_in_order((op,))
355 +
        operation_descendents = itertools.filterfalse(qml.circuit_graph._is_observable, succ)
356 +
        observable_descendents = filter(qml.circuit_graph._is_observable, succ)
357 +
358 +
        for BB in operation_descendents:
359 +
            if not BB.supports_heisenberg:
360 +
                # if the descendant gate is non-Gaussian in parameter-shift differentiation
361 +
                # mode, then there must be no observable following it.
362 +
                continue
363 +
364 +
            B = BB.heisenberg_tr(dev_wires) @ B
365 +
            B_inv = B_inv @ BB.heisenberg_tr(dev_wires, inverse=True)
366 +
367 +
        Z = B @ Z @ B_inv  # conjugation
368 +
369 +
        g_tape = tape.copy(copy_operations=True)
370 +
        constants = []
371 +
372 +
        # transform the descendant observables into their derivatives using Z
373 +
        transformed_obs_idx = []
374 +
375 +
        for obs in observable_descendents:
376 +
            # get the index of the descendent observable
377 +
            idx = tape.observables.index(obs)
378 +
            transformed_obs_idx.append(idx)
379 +
380 +
            transformed_obs = _transform_observable(obs, Z, dev_wires)
381 +
382 +
            A = transformed_obs.parameters[0]
383 +
            constant = None
384 +
385 +
            # Check if the transformed observable corresponds to a constant term.
386 +
            if len(A.nonzero()[0]) == 1:
387 +
                if A.ndim == 2 and A[0, 0] != 0:
388 +
                    constant = A[0, 0]
389 +
390 +
                elif A.ndim == 1 and A[0] != 0:
391 +
                    constant = A[0]
392 +
393 +
            constants.append(constant)
394 +
395 +
            g_tape._measurements[idx] = qml.measure.MeasurementProcess(
396 +
                qml.operation.Expectation, _transform_observable(obs, Z, dev_wires)
397 +
            )
398 +
399 +
        if not any(i is None for i in constants):
400 +
            # Check if *all* transformed observables corresponds to a constant term.
401 +
            # term. If this is the case for all transformed observables on the tape,
402 +
            # then <psi|A|psi> = A<psi|psi> = A,
403 +
            # and we can avoid the device execution.
404 +
            shapes[-1] = 0
405 +
            obs_indices.append(transformed_obs_idx)
406 +
            gradient_values.append(constants)
407 +
            continue
408 +
409 +
        gradient_tapes.append(g_tape)
410 +
        obs_indices.append(transformed_obs_idx)
411 +
        gradient_values.append(None)
412 +
413 +
    def processing_fn(results):
414 +
        grads = []
415 +
        start = 0
416 +
417 +
        if not results:
418 +
            results = [np.zeros([tape.output_dim])]
419 +
420 +
        interface = qml.math.get_interface(results[0])
421 +
        iterator = enumerate(zip(shapes, gradient_values, obs_indices))
422 +
423 +
        for i, (shape, grad_value, obs_ind) in iterator:
424 +
425 +
            if shape == 0:
426 +
                # parameter has zero gradient
427 +
                g = qml.math.zeros_like(results[0], like=interface)
428 +
429 +
                if grad_value:
430 +
                    g = qml.math.scatter_element_add(g, obs_ind, grad_value, like=interface)
431 +
432 +
                grads.append(g)
433 +
                continue
434 +
435 +
            obs_result = results[start : start + shape]
436 +
            start = start + shape
437 +
438 +
            # compute the linear combination of results and coefficients
439 +
            obs_result = qml.math.stack(obs_result[0])
440 +
            g = qml.math.zeros_like(obs_result, like=interface)
441 +
442 +
            if qml.math.get_interface(g) not in ("tensorflow", "autograd"):
443 +
                obs_ind = (obs_ind,)
444 +
445 +
            g = qml.math.scatter_element_add(g, obs_ind, obs_result[obs_ind], like=interface)
446 +
            grads.append(g)
447 +
448 +
        # The following is for backwards compatibility; currently,
449 +
        # the device stacks multiple measurement arrays, even if not the same
450 +
        # size, resulting in a ragged array.
451 +
        # In the future, we might want to change this so that only tuples
452 +
        # of arrays are returned.
453 +
        for i, g in enumerate(grads):
454 +
            g = qml.math.convert_like(g, results[0])
455 +
            if hasattr(g, "dtype") and g.dtype is np.dtype("object"):
456 +
                grads[i] = qml.math.hstack(g)
457 +
458 +
        return qml.math.T(qml.math.stack(grads))
459 +
460 +
    return gradient_tapes, processing_fn
461 +
462 +
463 +
def param_shift_cv(
464 +
    tape,
465 +
    dev,
466 +
    argnum=None,
467 +
    shift=np.pi / 2,
468 +
    gradient_recipes=None,
469 +
    fallback_fn=finite_diff,
470 +
    f0=None,
471 +
    force_order2=False,
472 +
):
473 +
    r"""Generate the CV parameter-shift tapes and postprocessing methods required
474 +
    to compute the gradient of a gate parameter with respect to the CV output.
475 +
476 +
    Args:
477 +
        tape (.QuantumTape): quantum tape to differentiate
478 +
        dev (.Device): device the parameter-shift method is to be computed on
479 +
        argnum (int or list[int] or None): Trainable parameter indices to differentiate
480 +
            with respect to. If not provided, the derivative with respect to all
481 +
            trainable indices are returned.
482 +
        shift (float): The shift value to use for the two-term parameter-shift formula.
483 +
            Only valid if the operation in question supports the two-term parameter-shift
484 +
            rule (that is, it has two distinct eigenvalues) and ``gradient_recipes``
485 +
            is ``None``.
486 +
        gradient_recipes (tuple(list[list[float]] or None)): List of gradient recipes
487 +
            for the parameter-shift method. One gradient recipe must be provided
488 +
            per trainable parameter.
489 +
490 +
            This is a tuple with one nested list per parameter. For
491 +
            parameter :math:`\phi_k`, the nested list contains elements of the form
492 +
            :math:`[c_i, a_i, s_i]` where :math:`i` is the index of the
493 +
            term, resulting in a gradient recipe of
494 +
495 +
            .. math:: \frac{\partial}{\partial\phi_k}f = \sum_{i} c_i f(a_i \phi_k + s_i).
496 +
497 +
            If ``None``, the default gradient recipe containing the two terms
498 +
            :math:`[c_0, a_0, s_0]=[1/2, 1, \pi/2]` and :math:`[c_1, a_1,
499 +
            s_1]=[-1/2, 1, -\pi/2]` is assumed for every parameter.
500 +
        fallback_fn (None or Callable): a fallback grdient function to use for
501 +
            any parameters that do not support the parameter-shift rule.
502 +
        f0 (tensor_like[float] or None): Output of the evaluated input tape. If provided,
503 +
            and the gradient recipe contains an unshifted term, this value is used,
504 +
            saving a quantum evaluation.
505 +
        force_order2 (bool): if True, use the order-2 method even if not necessary
506 +
507 +
    Returns:
508 +
        tuple[list[QuantumTape], function]: A tuple containing a
509 +
        list of generated tapes, in addition to a post-processing
510 +
        function to be applied to the evaluated tapes.
511 +
512 +
    This transform supports analytic gradients of Gaussian CV operations using
513 +
    the parameter-shift rule. This gradient method returns *exact* gradients,
514 +
    and can be computed directly on quantum hardware.
515 +
516 +
    Analytic gradients of photonic circuits that satisfy
517 +
    the following constraints with regards to measurements are supported:
518 +
519 +
    * Expectation values are restricted to observables that are first- and
520 +
      second-order in :math:`\hat{x}` and :math:`\hat{p}` only.
521 +
      This includes :class:`~.X`, :class:`~.P`, :class:`~.QuadOperator`,
522 +
      :class:`~.PolyXP`, and :class:`~.NumberOperator`.
523 +
524 +
      For second-order observables, the device **must support** :class:`~.PolyXP`.
525 +
526 +
    * Variances are restricted to observables that are first-order
527 +
      in :math:`\hat{x}` and :math:`\hat{p}` only. This includes :class:`~.X`, :class:`~.P`,
528 +
      :class:`~.QuadOperator`, and *some* parameter values of :class:`~.PolyXP`.
529 +
530 +
      The device **must support** :class:`~.PolyXP`.
531 +
532 +
    .. warning::
533 +
534 +
        Fock state probabilities (tapes that return :func:`~pennylane.probs` or
535 +
        expectation values of :class:`~.FockStateProjector`) are not supported.
536 +
537 +
    In addition, the tape operations must fulfill the following requirements:
538 +
539 +
    * Only Gaussian operations are differentiable.
540 +
541 +
    * Non-differentiable Fock states and Fock operations may *precede* all differentiable Gaussian,
542 +
      operations. For example, the following is permissible:
543 +
544 +
      .. code-block:: python
545 +
546 +
          with qml.tape.JacobianTape() as tape:
547 +
              # Non-differentiable Fock operations
548 +
              qml.FockState(2, wires=0)
549 +
              qml.Kerr(0.654, wires=1)
550 +
551 +
              # differentiable Gaussian operations
552 +
              qml.Displacement(0.6, 0.5, wires=0)
553 +
              qml.Beamsplitter(0.5, 0.1, wires=[0, 1])
554 +
              qml.expval(qml.NumberOperator(0))
555 +
556 +
          tape.trainable_params = {2, 3, 4}
557 +
558 +
    * If a Fock operation succeeds a Gaussian operation, the Fock operation must
559 +
      not contribute to any measurements. For example, the following is allowed:
560 +
561 +
      .. code-block:: python
562 +
563 +
          with qml.tape.JacobianTape() as tape:
564 +
              qml.Displacement(0.6, 0.5, wires=0)
565 +
              qml.Beamsplitter(0.5, 0.1, wires=[0, 1])
566 +
              qml.Kerr(0.654, wires=1)  # there is no measurement on wire 1
567 +
              qml.expval(qml.NumberOperator(0))
568 +
569 +
          tape.trainable_params = {0, 1, 2}
570 +
571 +
    If any of the above constraints are not followed, the tape cannot be differentiated
572 +
    via the CV parameter-shift rule. Please use numerical differentiation instead.
573 +
574 +
    **Example**
575 +
576 +
    >>> r0, phi0, r1, phi1 = [0.4, -0.3, -0.7, 0.2]
577 +
    >>> dev = qml.device("default.gaussian", wires=1)
578 +
    >>> with qml.tape.JacobianTape() as tape:
579 +
    ...     qml.Squeezing(r0, phi0, wires=[0])
580 +
    ...     qml.Squeezing(r1, phi1, wires=[0])
581 +
    ...     qml.expval(qml.NumberOperator(0))  # second-order
582 +
    >>> tape.trainable_params = {0, 2}
583 +
    >>> gradient_tapes, fn = qml.gradients.param_shift_cv(tape, dev)
584 +
    >>> res = dev.batch_execute(gradient_tapes)
585 +
    >>> fn(res)
586 +
    array([[-0.32487113, -0.87049853]])
587 +
    """
588 +
589 +
    # perform gradient method validation
590 +
    if any(m.return_type is qml.operation.State for m in tape.measurements):
591 +
        raise ValueError(
592 +
            "Computing the gradient of circuits that return the state is not supported."
593 +
        )
594 +
595 +
    _gradient_analysis(tape)
596 +
597 +
    gradient_tapes = []
598 +
    shapes = []
599 +
    fns = []
600 +
601 +
    def _update(data):
602 +
        """Utility function to update the list of gradient tapes,
603 +
        the corresponding number of gradient tapes, and the processing functions"""
604 +
        gradient_tapes.extend(data[0])
605 +
        shapes.append(len(data[0]))
606 +
        fns.append(data[1])
607 +
608 +
    # TODO: replace the JacobianTape._grad_method_validation
609 +
    # functionality before deprecation.
610 +
    diff_methods = tape._grad_method_validation("analytic" if fallback_fn is None else "best")
611 +
    all_params_grad_method_zero = all(g == "0" for g in diff_methods)
612 +
613 +
    if not tape.trainable_params or all_params_grad_method_zero:
614 +
        return gradient_tapes, lambda _: np.zeros([tape.output_dim, len(tape.trainable_params)])
615 +
616 +
    # TODO: replace the JacobianTape._choose_params_with_methods
617 +
    # functionality before deprecation.
618 +
    method_map = dict(tape._choose_params_with_methods(diff_methods, argnum))
619 +
    var_present = any(m.return_type is qml.operation.Variance for m in tape.measurements)
620 +
621 +
    unsupported_params = []
622 +
    first_order_params = []
623 +
    second_order_params = []
624 +
625 +
    for idx, g in method_map.items():
626 +
        if g == "F":
627 +
            unsupported_params.append(idx)
628 +
629 +
        elif g == "A":
630 +
            first_order_params.append(idx)
631 +
632 +
        elif g == "A2":
633 +
            second_order_params.append(idx)
634 +
635 +
    if force_order2:
636 +
        # all analytic parameters should be computed using the second-order method
637 +
        second_order_params += first_order_params
638 +
        first_order_params = []
639 +
640 +
    if "PolyXP" not in dev.observables and (second_order_params or var_present):
641 +
        warnings.warn(
642 +
            f"The device {dev.short_name} does not support "
643 +
            "the PolyXP observable. The analytic parameter-shift cannot be used for "
644 +
            "second-order observables; falling back to finite-differences.",
645 +
            UserWarning,
646 +
        )
647 +
648 +
        if var_present:
649 +
            unsupported_params += first_order_params
650 +
            first_order_params = []
651 +
652 +
        unsupported_params += second_order_params
653 +
        second_order_params = []
654 +
655 +
    # If there are unsupported operations, call the fallback gradient function
656 +
    if unsupported_params:
657 +
        _update(fallback_fn(tape, argnum=unsupported_params))
658 +
659 +
    # collect all the analytic parameters
660 +
    argnum = first_order_params + second_order_params
661 +
662 +
    if not argnum:
663 +
        # No analytic parameters. Return the existing fallback tapes/fn
664 +
        return gradient_tapes, fns[-1]
665 +
666 +
    gradient_recipes = gradient_recipes or [None] * len(argnum)
667 +
668 +
    if var_present:
669 +
        _update(var_param_shift(tape, dev.wires, argnum, shift, gradient_recipes, f0))
670 +
671 +
    else:
672 +
        # Only expectation values were specified
673 +
        if first_order_params:
674 +
            _update(expval_param_shift(tape, first_order_params, shift, gradient_recipes, f0))
675 +
676 +
        if second_order_params:
677 +
            _update(
678 +
                second_order_param_shift(
679 +
                    tape, dev.wires, second_order_params, shift, gradient_recipes
680 +
                )
681 +
            )
682 +
683 +
    def processing_fn(results):
684 +
        start = 0
685 +
        grads = []
686 +
687 +
        for s, f in zip(shapes, fns):
688 +
            grads.append(f(results[start : start + s]))
689 +
            start += s
690 +
691 +
        return sum(grads)
692 +
693 +
    return gradient_tapes, processing_fn

@@ -16,6 +16,8 @@
Loading
16 16
17 17
from . import finite_difference
18 18
from . import parameter_shift
19 +
from . import parameter_shift_cv
19 20
20 21
from .finite_difference import finite_diff, finite_diff_coeffs, generate_shifted_tapes
21 22
from .parameter_shift import param_shift
23 +
from .parameter_shift_cv import param_shift_cv

@@ -399,7 +399,8 @@
Loading
399 399
        """
400 400
        raise NotImplementedError
401 401
402 -
    def _choose_params_with_methods(self, diff_methods, argnum):
402 +
    @staticmethod
403 +
    def _choose_params_with_methods(diff_methods, argnum):
403 404
        """Chooses the trainable parameters to use for computing the Jacobian
404 405
        by returning a map of their indices and differentiation methods.
405 406
@@ -423,11 +424,6 @@
Loading
423 424
        if isinstance(argnum, int):
424 425
            argnum = [argnum]
425 426
426 -
        if not all(ind in self.trainable_params for ind in argnum):
427 -
            raise ValueError(
428 -
                "Incorrect trainable parameters were specified for the argnum argument."
429 -
            )
430 -
431 427
        num_params = len(argnum)
432 428
433 429
        if num_params == 0:

@@ -161,7 +161,7 @@
Loading
161 161
        ``idx`` shifted by consecutive values of ``shift``. The length
162 162
        of the returned list of tapes will match the length of ``shifts``.
163 163
    """
164 -
    params = tape.get_parameters()
164 +
    params = list(tape.get_parameters())
165 165
    tapes = []
166 166
167 167
    for i, s in enumerate(shifts):
@@ -300,7 +300,7 @@
Loading
300 300
        # In the future, we might want to change this so that only tuples
301 301
        # of arrays are returned.
302 302
        for i, g in enumerate(grads):
303 -
            g = qml.math.convert_like(g, res[0])
303 +
            g = qml.math.convert_like(g, results[0])
304 304
            if hasattr(g, "dtype") and g.dtype is np.dtype("object"):
305 305
                grads[i] = qml.math.hstack(g)
306 306

@@ -59,8 +59,13 @@
Loading
59 59
        """
60 60
        shifted_args = list(args)
61 61
62 +
        trainable_args = []
63 +
        for arg in args:
64 +
            if getattr(arg, "requires_grad", True):
65 +
                trainable_args.append(arg)
66 +
62 67
        if self.accumulation:
63 -
            for index, arg in enumerate(args):
68 +
            for index, arg in enumerate(trainable_args):
64 69
                if self.accumulation[index]:
65 70
                    x_flat = _flatten(arg)
66 71
                    acc = _flatten(self.accumulation[index])
@@ -82,7 +87,7 @@
Loading
82 87
        grad = g(*shifted_args, **kwargs)
83 88
        forward = getattr(g, "forward", None)
84 89
85 -
        if len(args) == 1:
90 +
        if len(trainable_args) == 1:
86 91
            grad = (grad,)
87 92
88 93
        return grad, forward

@@ -129,7 +129,12 @@
Loading
129 129
        grad = g(*args, **kwargs)
130 130
        forward = getattr(g, "forward", None)
131 131
132 -
        if len(args) == 1:
132 +
        num_trainable_args = 0
133 +
        for arg in args:
134 +
            if getattr(arg, "requires_grad", True):
135 +
                num_trainable_args += 1
136 +
137 +
        if num_trainable_args == 1:
133 138
            grad = (grad,)
134 139
135 140
        return grad, forward

@@ -1692,7 +1692,7 @@
Loading
1692 1692
        Returns:
1693 1693
            array[float]: :math:`\tilde{U}`, the Heisenberg picture representation of the linear transformation
1694 1694
        """
1695 -
        p = self.parameters
1695 +
        p = [qml.math.toarray(a) for a in self.parameters]
1696 1696
        if inverse:
1697 1697
            if self.par_domain == "A":
1698 1698
                # TODO: expand this for the new par domain class, for non-unitary matrices.
Files Coverage
pennylane 98.22%
qchem/pennylane_qchem 98.37%
Project Totals (184 files) 98.22%
1
fixes:
2
  - "pennylane_qchem/::qchem/pennylane_qchem/"
3

4
ignore:
5
  - "pennylane/devices/tests/*"
6

7
codecov:
8
  notify:
9
    after_n_builds: 9
10

11
comment:
12
  after_n_builds: 9
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading